diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..833d1d4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*.class
+.DS_Store
+
+
diff --git a/assembly.xml b/assembly.xml
new file mode 100644
index 0000000..4074c72
--- /dev/null
+++ b/assembly.xml
@@ -0,0 +1,27 @@
+
+ * Cameras are also the point through which all PInputEvents enter Piccolo. The + * canvas coordinate system and the local coordinate system of the topmost + * camera should always be the same. + *
+ * + * @see PLayer + * @version 1.0 + * @author Jesse Grosjean + */ +public class PCamera extends PNode { + + /** Default serial version UID. */ + private static final long serialVersionUID = 1L; + + /** + * The property name that identifies a change in the set of this camera's + * layers (see {@link #getLayer getLayer}, {@link #getLayerCount + * getLayerCount}, {@link #getLayersReference getLayersReference}). A + * property change event's new value will be a reference to the list of this + * nodes layers, but old value will always be null. + */ + public static final String PROPERTY_LAYERS = "layers"; + + /** + * The property code that identifies a change in the set of this camera's + * layers (see {@link #getLayer getLayer}, {@link #getLayerCount + * getLayerCount}, {@link #getLayersReference getLayersReference}). A + * property change event's new value will be a reference to the list of this + * nodes layers, but old value will always be null. + */ + public static final int PROPERTY_CODE_LAYERS = 1 << 11; + + /** + * The property name that identifies a change in this camera's view + * transform (see {@link #getViewTransform getViewTransform}, + * {@link #getViewTransformReference getViewTransformReference}). A property + * change event's new value will be a reference to the view transform, but + * old value will always be null. + */ + public static final String PROPERTY_VIEW_TRANSFORM = "viewTransform"; + + /** + * The property code that identifies a change in this camera's view + * transform (see {@link #getViewTransform getViewTransform}, + * {@link #getViewTransformReference getViewTransformReference}). A property + * change event's new value will be a reference to the view transform, but + * old value will always be null. + */ + public static final int PROPERTY_CODE_VIEW_TRANSFORM = 1 << 12; + + /** Denotes that the view has no constraints. */ + public static final int VIEW_CONSTRAINT_NONE = 0; + + /** Enforces that the view be able to see all nodes in the scene. */ + public static final int VIEW_CONSTRAINT_ALL = 1; + + /** Constrains the the view to be centered on the scene's full bounds. */ + public static final int VIEW_CONSTRAINT_CENTER = 2; + + /** Component which receives repaint notification from this camera. */ + private transient PComponent component; + + /** List of layers viewed by this camera. */ + private transient List/*null
if no
+ * component has been associated with this camera, as may be the case for
+ * internal cameras.
+ *
+ * @return the component for this camera, or null
if no such
+ * component exists
+ */
+ public PComponent getComponent() {
+ return component;
+ }
+
+ /**
+ * Set the component for this camera to component
. The
+ * component, if non-null, receives repaint notification from this camera.
+ *
+ * @param component component for this camera
+ */
+ public void setComponent(final PComponent component) {
+ this.component = component;
+ invalidatePaint();
+ }
+
+ /**
+ * Repaint this camera and forward the repaint request to the component
+ * for this camera, if it is non-null.
+ *
+ * @param localBounds bounds that require repainting, in local coordinates
+ * @param sourceNode node from which the repaint message originates, may
+ * be the camera itself
+ */
+ public void repaintFrom(final PBounds localBounds, final PNode sourceNode) {
+ if (getParent() != null) {
+ if (sourceNode != this) {
+ localToParent(localBounds);
+ }
+ if (component != null) {
+ component.repaint(localBounds);
+ }
+ getParent().repaintFrom(localBounds, this);
+ }
+ }
+
+ /**
+ * Repaint from one of the camera's layers. The repaint region needs to be
+ * transformed from view to local in this case. Unlike most repaint methods
+ * in Piccolo2D this one must not modify the viewBounds
+ * parameter.
+ *
+ * @since 1.3
+ * @param viewBounds bounds that require repainting, in view coordinates
+ * @param repaintedLayer layer dispatching the repaint notification
+ */
+ public void repaintFromLayer(final PBounds viewBounds, final PLayer repaintedLayer) {
+ TEMP_REPAINT_RECT.setRect(viewBounds);
+ viewToLocal(TEMP_REPAINT_RECT);
+ if (getBoundsReference().intersects(TEMP_REPAINT_RECT)) {
+ Rectangle2D.intersect(TEMP_REPAINT_RECT, getBoundsReference(), TEMP_REPAINT_RECT);
+ repaintFrom(TEMP_REPAINT_RECT, repaintedLayer);
+ }
+ }
+
+ /**
+ * @deprecated by {@link #repaintFromLayer(PBounds, PLayer)}. Will be removed
+ * in version 2.0.
+ * @param viewBounds bounds that require repainting, in view coordinates
+ * @param repaintedLayer layer dispatching the repaint notification
+ */
+ public void repaintFromLayer(final PBounds viewBounds, final PNode repaintedLayer) {
+ throw new IllegalArgumentException("repaintedLayer not an instance of PLayer");
+ }
+
+ /**
+ * Return a reference to the list of layers viewed by this camera.
+ *
+ * @return the list of layers viewed by this camera
+ */
+ public List/*index < 0 || index >= getLayerCount()
)
+ */
+ public PLayer getLayer(final int index) {
+ return (PLayer) layers.get(index);
+ }
+
+ /**
+ * Return the index of the first occurrence of the specified layer in the
+ * list of layers viewed by this camera, or -1
if the list of layers
+ * viewed by this camera does not contain the specified layer.
+ *
+ * @param layer layer to search for
+ * @return the index of the first occurrence of the specified layer in the
+ * list of layers viewed by this camera, or -1
if the list of
+ * layers viewed by this camera does not contain the specified layer
+ */
+ public int indexOfLayer(final PLayer layer) {
+ return layers.indexOf(layer);
+ }
+
+ /**
+ * Inserts the specified layer at the end of the list of layers viewed by this camera.
+ * Layers may be viewed by multiple cameras at once.
+ *
+ * @param layer layer to add
+ */
+ public void addLayer(final PLayer layer) {
+ addLayer(layers.size(), layer);
+ }
+
+ /**
+ * Inserts the specified layer at the specified position in the list of layers viewed by this camera.
+ * Layers may be viewed by multiple cameras at once.
+ *
+ * @param index index at which the specified layer is to be inserted
+ * @param layer layer to add
+ * @throws IndexOutOfBoundsException if the specified index is out of range
+ * (index < 0 || index >= getLayerCount()
)
+ */
+ public void addLayer(final int index, final PLayer layer) {
+ layers.add(index, layer);
+ layer.addCamera(this);
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_LAYERS, PROPERTY_LAYERS, null, layers);
+ }
+
+ /**
+ * Removes the first occurrence of the specified layer from the list of
+ * layers viewed by this camera, if it is present.
+ *
+ * @param layer layer to be removed
+ * @return the specified layer
+ */
+ public PLayer removeLayer(final PLayer layer) {
+ layer.removeCamera(this);
+ if (layers.remove(layer)) {
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_LAYERS, PROPERTY_LAYERS, null, layers);
+ }
+ return layer;
+ }
+
+ /**
+ * Removes the element at the specified position from the list of layers
+ * viewed by this camera.
+ *
+ * @param index index of the layer to remove
+ * @return the layer previously at the specified position
+ * @throws IndexOutOfBoundsException if the specified index is out of range
+ * (index < 0 || index >= getLayerCount()
)
+ */
+ public PLayer removeLayer(final int index) {
+ final PLayer layer = (PLayer) layers.remove(index);
+ layer.removeCamera(this);
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_LAYERS, PROPERTY_LAYERS, null, layers);
+ return layer;
+ }
+
+ /**
+ * Return the union of the full bounds of each layer in the list of layers
+ * viewed by this camera, or empty bounds if the list of layers viewed by
+ * this camera is empty.
+ *
+ * @return the union of the full bounds of each layer in the list of layers
+ * viewed by this camera, or empty bounds if the list of layers viewed
+ * by this camera is empty
+ */
+ public PBounds getUnionOfLayerFullBounds() {
+ final PBounds result = new PBounds();
+ final int size = layers.size();
+ for (int i = 0; i < size; i++) {
+ final PLayer each = (PLayer) layers.get(i);
+ result.add(each.getFullBoundsReference());
+ }
+ return result;
+ }
+
+ /**
+ * Paint this camera and then paint this camera's view through its view
+ * transform.
+ *
+ * @param paintContext context in which painting occurs
+ */
+ protected void paint(final PPaintContext paintContext) {
+ super.paint(paintContext);
+
+ paintContext.pushClip(getBoundsReference());
+ paintContext.pushTransform(viewTransform);
+
+ paintCameraView(paintContext);
+ paintDebugInfo(paintContext);
+
+ paintContext.popTransform(viewTransform);
+ paintContext.popClip(getBoundsReference());
+ }
+
+ /**
+ * Paint all the layers in the list of layers viewed by this camera. This method
+ * is called after the view transform and clip have been applied to the
+ * specified paint context.
+ *
+ * @param paintContext context in which painting occurs
+ */
+ protected void paintCameraView(final PPaintContext paintContext) {
+ final int size = layers.size();
+ for (int i = 0; i < size; i++) {
+ final PLayer each = (PLayer) layers.get(i);
+ each.fullPaint(paintContext);
+ }
+ }
+
+ /**
+ * Renders debug info onto the newly painted scene. Things like full bounds
+ * and bounds are painted as filled and outlines.
+ *
+ * @param paintContext context in which painting occurs
+ */
+ protected void paintDebugInfo(final PPaintContext paintContext) {
+ if (PDebug.debugBounds || PDebug.debugFullBounds) {
+ final Graphics2D g2 = paintContext.getGraphics();
+ paintContext.setRenderQuality(PPaintContext.LOW_QUALITY_RENDERING);
+ g2.setStroke(new BasicStroke(0));
+ final ArrayList nodes = new ArrayList();
+ final PBounds nodeBounds = new PBounds();
+
+ final Color boundsColor = Color.red;
+ final Color fullBoundsColor = new Color(1.0f, 0f, 0f, 0.2f);
+
+ final int size = layers.size();
+ for (int i = 0; i < size; i++) {
+ ((PLayer) layers.get(i)).getAllNodes(null, nodes);
+ }
+
+ final Iterator i = getAllNodes(null, nodes).iterator();
+
+ while (i.hasNext()) {
+ final PNode each = (PNode) i.next();
+
+ if (PDebug.debugBounds) {
+ g2.setPaint(boundsColor);
+ nodeBounds.setRect(each.getBoundsReference());
+
+ if (!nodeBounds.isEmpty()) {
+ each.localToGlobal(nodeBounds);
+ globalToLocal(nodeBounds);
+ if (each == this || each.isDescendentOf(this)) {
+ localToView(nodeBounds);
+ }
+ g2.draw(nodeBounds);
+ }
+ }
+
+ if (PDebug.debugFullBounds) {
+ g2.setPaint(fullBoundsColor);
+ nodeBounds.setRect(each.getFullBoundsReference());
+
+ if (!nodeBounds.isEmpty()) {
+ if (each.getParent() != null) {
+ each.getParent().localToGlobal(nodeBounds);
+ }
+ globalToLocal(nodeBounds);
+ if (each == this || each.isDescendentOf(this)) {
+ localToView(nodeBounds);
+ }
+ g2.fill(nodeBounds);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * + * Pushes this camera onto the specified paint context so that it + * can be accessed later by {@link PPaintContext#getCamera}. + *
+ */ + public void fullPaint(final PPaintContext paintContext) { + paintContext.pushCamera(this); + super.fullPaint(paintContext); + paintContext.popCamera(); + } + + /** + * Generate and return a PPickPath for the point x,y specified in the local + * coord system of this camera. Picking is done with a rectangle, halo + * specifies how large that rectangle will be. + * + * @param x the x coordinate of the pick path given in local coordinates + * @param y the y coordinate of the pick path given in local coordinates + * @param halo the distance from the x,y coordinate that is considered for + * inclusion in the pick path + * + * @return the picked path + */ + public PPickPath pick(final double x, final double y, final double halo) { + final PBounds b = new PBounds(new Point2D.Double(x, y), -halo, -halo); + final PPickPath result = new PPickPath(this, b); + + fullPick(result); + + // make sure this camera is pushed. + if (result.getNodeStackReference().size() == 0) { + result.pushNode(this); + result.pushTransform(getTransformReference(false)); + } + + return result; + } + + /** + * {@inheritDoc} + * + *+ * After the direct children of this camera have been given a chance to be + * picked all of the layers in the list of layers viewed by this camera are + * given a chance to be picked. + *
+ * + * @return true if any of the layers in the list of layers viewed by this + * camera were picked + */ + protected boolean pickAfterChildren(final PPickPath pickPath) { + if (intersects(pickPath.getPickBounds())) { + pickPath.pushTransform(viewTransform); + + if (pickCameraView(pickPath)) { + return true; + } + + pickPath.popTransform(viewTransform); + return true; + } + return false; + } + + /** + * Try to pick all of the layers in the list of layers viewed by this + * camera. This method is called after the view transform has been applied + * to the specified pick path. + * + * @param pickPath pick path + * @return true if any of the layers in the list of layers viewed by this + * camera were picked + */ + protected boolean pickCameraView(final PPickPath pickPath) { + final int size = layers.size(); + for (int i = size - 1; i >= 0; i--) { + final PLayer each = (PLayer) layers.get(i); + if (each.fullPick(pickPath)) { + return true; + } + } + return false; + } + + // **************************************************************** + // View Transform - Methods for accessing the view transform. The + // view transform is applied before painting and picking the cameras + // layers. But not before painting or picking its direct children. + // + // Changing the view transform is how zooming and panning are + // accomplished. + // **************************************************************** + + /** + * Return the bounds of this camera in the view coordinate system. + * + * @return the bounds of this camera in the view coordinate system + */ + public PBounds getViewBounds() { + return (PBounds) localToView(getBounds()); + } + + /** + * Animates the camera's view so that the given bounds (in camera layer's + * coordinate system) are centered within the cameras view bounds. Use this + * method to point the camera at a given location. + * + * @param centerBounds the targetBounds + */ + public void setViewBounds(final Rectangle2D centerBounds) { + animateViewToCenterBounds(centerBounds, true, 0); + } + + /** + * Return the scale applied by the view transform to the list of layers + * viewed by this camera. + * + * @return the scale applied by the view transform to the list of layers + * viewed by this camera + */ + public double getViewScale() { + return viewTransform.getScale(); + } + + /** + * Scale the view transform applied to the list of layers viewed by this + * camera byscale
about the point [0, 0]
.
+ *
+ * @param scale view transform scale
+ */
+ public void scaleView(final double scale) {
+ scaleViewAboutPoint(scale, 0, 0);
+ }
+
+ /**
+ * Scale the view transform applied to the list of layers viewed by this
+ * camera by scale
about the specified point
+ * [x, y]
.
+ *
+ * @param scale view transform scale
+ * @param x scale about point, x coordinate
+ * @param y scale about point, y coordinate
+ */
+ public void scaleViewAboutPoint(final double scale, final double x, final double y) {
+ viewTransform.scaleAboutPoint(scale, x, y);
+ applyViewConstraints();
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_VIEW_TRANSFORM, PROPERTY_VIEW_TRANSFORM, null, viewTransform);
+ }
+
+ /**
+ * Set the scale applied by the view transform to the list of layers
+ * viewed by this camera to scale
.
+ *
+ * @param scale view transform scale
+ */
+ public void setViewScale(final double scale) {
+ scaleView(scale / getViewScale());
+ }
+
+ /**
+ * Translate the view transform applied to the list of layers viewed by this
+ * camera by [dx, dy]
.
+ *
+ * @param dx translate delta x
+ * @param dy translate delta y
+ */
+ public void translateView(final double dx, final double dy) {
+ viewTransform.translate(dx, dy);
+ applyViewConstraints();
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_VIEW_TRANSFORM, PROPERTY_VIEW_TRANSFORM, null, viewTransform);
+ }
+
+ /**
+ * Offset the view transform applied to the list of layers viewed by this camera by [dx, dy]
. This is
+ * NOT effected by the view transform's current scale or rotation. This is implemented by directly adding dx to the
+ * m02 position and dy to the m12 position in the affine transform.
+ *
+ * @param dx offset delta x
+ * @param dy offset delta y
+ */
+ /*
+ public void offsetView(final double dx, final double dy) {
+ setViewOffset(viewTransform.getTranslateX() + dx, viewTransform.getTranslateY() + dy);
+ }
+ */
+
+ /**
+ * Set the offset for the view transform applied to the list of layers
+ * viewed by this camera to [x, y]
.
+ *
+ * @param x offset x
+ * @param y offset y
+ */
+ public void setViewOffset(final double x, final double y) {
+ viewTransform.setOffset(x, y);
+ applyViewConstraints();
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_VIEW_TRANSFORM, PROPERTY_VIEW_TRANSFORM, null, viewTransform);
+ }
+
+ /**
+ * Return a copy of the view transform applied to the list of layers
+ * viewed by this camera.
+ *
+ * @return a copy of the view transform applied to the list of layers
+ * viewed by this camera
+ */
+ public PAffineTransform getViewTransform() {
+ return (PAffineTransform) viewTransform.clone();
+ }
+
+ /**
+ * Return a reference to the view transform applied to the list of layers
+ * viewed by this camera.
+ *
+ * @return the view transform applied to the list of layers
+ * viewed by this camera
+ */
+ public PAffineTransform getViewTransformReference() {
+ return viewTransform;
+ }
+
+ /**
+ * Set the view transform applied to the list of layers
+ * viewed by this camera to viewTransform
.
+ *
+ * @param viewTransform view transform applied to the list of layers
+ * viewed by this camera
+ */
+ public void setViewTransform(final AffineTransform viewTransform) {
+ this.viewTransform.setTransform(viewTransform);
+ applyViewConstraints();
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_VIEW_TRANSFORM, PROPERTY_VIEW_TRANSFORM, null, this.viewTransform);
+ }
+
+ /**
+ * Animate the camera's view from its current transform when the activity
+ * starts to a new transform that centers the given bounds in the camera
+ * layer's coordinate system into the cameras view bounds. If the duration is
+ * 0 then the view will be transformed immediately, and null will be
+ * returned. Else a new PTransformActivity will get returned that is set to
+ * animate the camera's view transform to the new bounds. If shouldScale is
+ * true, then the camera will also scale its view so that the given bounds
+ * fit fully within the cameras view bounds, else the camera will maintain
+ * its original scale.
+ *
+ * @param centerBounds the bounds which the animation will pace at the
+ * center of the view
+ * @param shouldScaleToFit whether the camera should scale the view while
+ * animating to it
+ * @param duration how many milliseconds the animations should take
+ *
+ * @return the scheduled PTransformActivity
+ */
+ public PTransformActivity animateViewToCenterBounds(final Rectangle2D centerBounds, final boolean shouldScaleToFit,
+ final long duration) {
+ final PBounds viewBounds = getViewBounds();
+ final PDimension delta = viewBounds.deltaRequiredToCenter(centerBounds);
+ final PAffineTransform newTransform = getViewTransform();
+ newTransform.translate(delta.width, delta.height);
+
+ if (shouldScaleToFit) {
+ final double s = Math.min(viewBounds.getWidth() / centerBounds.getWidth(), viewBounds.getHeight()
+ / centerBounds.getHeight());
+ if (s != Double.POSITIVE_INFINITY && s != 0) {
+ newTransform.scaleAboutPoint(s, centerBounds.getCenterX(), centerBounds.getCenterY());
+ }
+ }
+
+ return animateViewToTransform(newTransform, duration);
+ }
+
+ /**
+ * Pan the camera's view from its current transform when the activity starts
+ * to a new transform so that the view bounds will contain (if possible,
+ * intersect if not possible) the new bounds in the camera layers coordinate
+ * system. If the duration is 0 then the view will be transformed
+ * immediately, and null will be returned. Else a new PTransformActivity
+ * will get returned that is set to animate the camera's view transform to
+ * the new bounds.
+ *
+ * @param panToBounds the bounds to which the view will animate to
+ * @param duration the duration of the animation given in milliseconds
+ *
+ * @return the scheduled PTransformActivity
+ */
+ public PTransformActivity animateViewToPanToBounds(final Rectangle2D panToBounds, final long duration) {
+ final PBounds viewBounds = getViewBounds();
+ final PDimension delta = viewBounds.deltaRequiredToContain(panToBounds);
+
+ if (delta.width != 0 || delta.height != 0) {
+ if (duration == 0) {
+ translateView(-delta.width, -delta.height);
+ }
+ else {
+ final AffineTransform at = getViewTransform();
+ at.translate(-delta.width, -delta.height);
+ return animateViewToTransform(at, duration);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Pan the camera's view from its current transform when the activity starts
+ * to a new transform so that the view bounds will contain (if possible,
+ * intersect if not possible) the new bounds in the camera layers coordinate
+ * system. If the duration is 0 then the view will be transformed
+ * immediately, and null will be returned. Else a new PTransformActivity
+ * will get returned that is set to animate the camera's view transform to
+ * the new bounds.
+ *
+ * @deprecated Renamed to animateViewToPanToBounds
+ *
+ * @param includeBounds the bounds to which the view will animate to
+ * @param duration the duration of the animation given in milliseconds
+ *
+ * @return the scheduled PTransformActivity
+ */
+ public PTransformActivity animateViewToIncludeBounds(final Rectangle2D includeBounds, final long duration) {
+ return animateViewToPanToBounds(includeBounds, duration);
+ }
+
+ /**
+ * Animate the cameras view transform from its current value when the
+ * activity starts to the new destination transform value.
+ *
+ * @param destination the transform to which the view should be transformed
+ * into
+ * @param duration the duraiton in milliseconds the animation should take
+ *
+ * @return the scheduled PTransformActivity
+ */
+ public PTransformActivity animateViewToTransform(final AffineTransform destination, final long duration) {
+ if (duration == 0) {
+ setViewTransform(destination);
+ return null;
+ }
+
+ final PTransformActivity.Target t = new PTransformActivity.Target() {
+ /** {@inheritDoc} */
+ public void setTransform(final AffineTransform aTransform) {
+ PCamera.this.setViewTransform(aTransform);
+ }
+
+ /** {@inheritDoc} */
+ public void getSourceMatrix(final double[] aSource) {
+ viewTransform.getMatrix(aSource);
+ }
+ };
+
+ final PTransformActivity transformActivity = new PTransformActivity(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE,
+ t, destination);
+
+ final PRoot r = getRoot();
+ if (r != null) {
+ r.getActivityScheduler().addActivity(transformActivity);
+ }
+
+ return transformActivity;
+ }
+
+ // ****************************************************************
+ // View Transform Constraints - Methods for setting and applying
+ // constraints to the view transform.
+ // ****************************************************************
+
+ /**
+ * Return the constraint applied to the view. The view constraint will be one of {@link #VIEW_CONSTRAINT_NONE},
+ * {@link #VIEW_CONSTRAINT_CENTER}, or {@link #VIEW_CONSTRAINT_CENTER}. Defaults to {@link #VIEW_CONSTRAINT_NONE}.
+ *
+ * @return the view constraint being applied to the view
+ */
+ public int getViewConstraint() {
+ return viewConstraint;
+ }
+
+ /**
+ * Set the view constraint to apply to the view to viewConstraint
. The view constraint must be one of
+ * {@link #VIEW_CONSTRAINT_NONE}, {@link #VIEW_CONSTRAINT_CENTER}, or {@link #VIEW_CONSTRAINT_CENTER}.
+ *
+ * @param viewConstraint constraint to apply to the view
+ * @throws IllegalArgumentException if viewConstraint
is not one of {@link #VIEW_CONSTRAINT_NONE},
+ * {@link #VIEW_CONSTRAINT_CENTER}, or {@link #VIEW_CONSTRAINT_CENTER}
+ */
+ public void setViewConstraint(final int viewConstraint) {
+ if (viewConstraint != VIEW_CONSTRAINT_NONE && viewConstraint != VIEW_CONSTRAINT_CENTER
+ && viewConstraint != VIEW_CONSTRAINT_ALL) {
+ throw new IllegalArgumentException("view constraint must be one "
+ + "of VIEW_CONSTRAINT_NONE, VIEW_CONSTRAINT_CENTER, or VIEW_CONSTRAINT_ALL");
+ }
+ this.viewConstraint = viewConstraint;
+ applyViewConstraints();
+ }
+
+ /**
+ * Transforms the view so that it conforms to the given constraint.
+ */
+ protected void applyViewConstraints() {
+ if (VIEW_CONSTRAINT_NONE == viewConstraint) {
+ return;
+ }
+ final PBounds viewBounds = getViewBounds();
+ final PBounds layerBounds = (PBounds) globalToLocal(getUnionOfLayerFullBounds());
+
+ if (VIEW_CONSTRAINT_CENTER == viewConstraint) {
+ layerBounds.setRect(layerBounds.getCenterX(), layerBounds.getCenterY(), 0, 0);
+ }
+ PDimension constraintDelta = viewBounds.deltaRequiredToContain(layerBounds);
+ viewTransform.translate(-constraintDelta.width, -constraintDelta.height);
+ }
+
+ // ****************************************************************
+ // Camera View Coord System Conversions - Methods to translate from
+ // the camera's local coord system (above the camera's view transform) to
+ // the
+ // camera view coord system (below the camera's view transform). When
+ // converting geometry from one of the canvas's layers you must go
+ // through the view transform.
+ // ****************************************************************
+
+ /**
+ * Convert the point from the camera's view coordinate system to the
+ * camera's local coordinate system. The given point is modified by this.
+ *
+ * @param viewPoint the point to transform to the local coordinate system
+ * from the view's coordinate system
+ * @return the transformed point
+ */
+ public Point2D viewToLocal(final Point2D viewPoint) {
+ return viewTransform.transform(viewPoint, viewPoint);
+ }
+
+ /**
+ * Convert the dimension from the camera's view coordinate system to the
+ * camera's local coordinate system. The given dimension is modified by
+ * this.
+ *
+ * @param viewDimension the dimension to transform from the view system to
+ * the local coordinate system
+ *
+ * @return returns the transformed dimension
+ */
+ public Dimension2D viewToLocal(final Dimension2D viewDimension) {
+ return viewTransform.transform(viewDimension, viewDimension);
+ }
+
+ /**
+ * Convert the rectangle from the camera's view coordinate system to the
+ * camera's local coordinate system. The given rectangle is modified by this
+ * method.
+ *
+ * @param viewRectangle the rectangle to transform from view to local
+ * coordinate System
+ * @return the transformed rectangle
+ */
+ public Rectangle2D viewToLocal(final Rectangle2D viewRectangle) {
+ return viewTransform.transform(viewRectangle, viewRectangle);
+ }
+
+ /**
+ * Convert the point from the camera's local coordinate system to the
+ * camera's view coordinate system. The given point is modified by this
+ * method.
+ *
+ * @param localPoint point to transform from local to view coordinate system
+ * @return the transformed point
+ */
+ public Point2D localToView(final Point2D localPoint) {
+ return viewTransform.inverseTransform(localPoint, localPoint);
+ }
+
+ /**
+ * Convert the dimension from the camera's local coordinate system to the
+ * camera's view coordinate system. The given dimension is modified by this
+ * method.
+ *
+ * @param localDimension the dimension to transform from local to view
+ * coordinate systems
+ * @return the transformed dimension
+ */
+ public Dimension2D localToView(final Dimension2D localDimension) {
+ return viewTransform.inverseTransform(localDimension, localDimension);
+ }
+
+ /**
+ * Convert the rectangle from the camera's local coordinate system to the
+ * camera's view coordinate system. The given rectangle is modified by this
+ * method.
+ *
+ * @param localRectangle the rectangle to transform from local to view
+ * coordinate system
+ * @return the transformed rectangle
+ */
+ public Rectangle2D localToView(final Rectangle2D localRectangle) {
+ return viewTransform.inverseTransform(localRectangle, localRectangle);
+ }
+
+ // ****************************************************************
+ // Serialization - Cameras conditionally serialize their layers.
+ // This means that only the layer references that were unconditionally
+ // (using writeObject) serialized by someone else will be restored
+ // when the camera is unserialized.
+ // ****************************************************************/
+
+ /**
+ * Write this camera and all its children out to the given stream. Note that
+ * the cameras layers are written conditionally, so they will only get
+ * written out if someone else writes them unconditionally.
+ *
+ * @param out the PObjectOutputStream to which this camera should be
+ * serialized
+ * @throws IOException if an error occured writing to the output stream
+ */
+ private void writeObject(final ObjectOutputStream out) throws IOException {
+ if (!(out instanceof PObjectOutputStream)) {
+ throw new RuntimeException("cannot serialize PCamera to a non PObjectOutputStream");
+ }
+ out.defaultWriteObject();
+
+ final int count = getLayerCount();
+ for (int i = 0; i < count; i++) {
+ ((PObjectOutputStream) out).writeConditionalObject(layers.get(i));
+ }
+
+ out.writeObject(Boolean.FALSE);
+ ((PObjectOutputStream) out).writeConditionalObject(component);
+ }
+
+ /**
+ * Deserializes this PCamera from the ObjectInputStream.
+ *
+ * @param in the source ObjectInputStream
+ * @throws IOException when error occurs during read
+ * @throws ClassNotFoundException if the stream attempts to deserialize a
+ * missing class
+ */
+ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+
+ layers = new ArrayList();
+
+ while (true) {
+ final Object each = in.readObject();
+ if (each != null) {
+ if (each.equals(Boolean.FALSE)) {
+ break;
+ }
+ else {
+ layers.add(each);
+ }
+ }
+ }
+
+ component = (PComponent) in.readObject();
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolo/PCanvas.java b/src/main/java/edu/umd/cs/piccolo/PCanvas.java
new file mode 100644
index 0000000..9198585
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/PCanvas.java
@@ -0,0 +1,941 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.KeyEventPostProcessor;
+import java.awt.KeyboardFocusManager;
+import java.awt.event.ActionListener;
+import java.awt.event.HierarchyEvent;
+import java.awt.event.HierarchyListener;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+
+import javax.swing.FocusManager;
+import javax.swing.JComponent;
+import javax.swing.RepaintManager;
+import javax.swing.Timer;
+
+import edu.umd.cs.piccolo.event.PInputEventListener;
+import edu.umd.cs.piccolo.event.PPanEventHandler;
+import edu.umd.cs.piccolo.event.PZoomEventHandler;
+import edu.umd.cs.piccolo.util.PBounds;
+import edu.umd.cs.piccolo.util.PDebug;
+import edu.umd.cs.piccolo.util.PPaintContext;
+import edu.umd.cs.piccolo.util.PStack;
+import edu.umd.cs.piccolo.util.PUtil;
+
+/**
+ * PCanvas is a simple Swing component that can be used to embed Piccolo
+ * into a Java Swing application. Canvases view the Piccolo scene graph through
+ * a camera. The canvas manages screen updates coming from this camera, and
+ * forwards swing mouse and keyboard events to the camera.
+ * + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PCanvas extends JComponent implements PComponent { + /** + * Allows for future serialization code to understand versioned binary + * formats. + */ + private static final long serialVersionUID = 1L; + + /** + * @deprecated this is a typo and clients should change their code to + * reflect the correct spelling + */ + public static final String INTERATING_CHANGED_NOTIFICATION = "INTERATING_CHANGED_NOTIFICATION"; + + /** + * The property name that identifies a change in the interacting state. + * + * @since 1.3 + * @deprecated in favor of PROPERTY_INTERACTING + */ + public static final String INTERACTING_CHANGED_NOTIFICATION = "INTERACTING_CHANGED_NOTIFICATION"; + + /** + * The property name that identifies a change in the interacting state. + * + * @since 1.3 + */ + public static final String PROPERTY_INTERACTING = "INTERACTING_CHANGED_NOTIFICATION"; + + /** + * Used as a public global to track the current canvas. + * + * @deprecated since it falsely assumes that there is only one PCanvas per + * program + */ + public static PCanvas CURRENT_ZCANVAS = null; + + /** The camera though which this Canvas is viewing. */ + private PCamera camera; + + /** + * Stack of cursors used to keep track of cursors as they change through + * interactions. + */ + private final PStack cursorStack; + + /** + * Whether the canvas is considered to be interacting, will probably mean + * worse render quality. + */ + private int interacting; + /** + * The render quality to use when the scene is not being interacted or + * animated. + */ + private int normalRenderQuality; + + /** The quality to use while the scene is being animated. */ + private int animatingRenderQuality; + + /** The quality to use while the scene is being interacted with. */ + private int interactingRenderQuality; + + /** The one and only pan handler. */ + private transient PPanEventHandler panEventHandler; + + /** The one and only ZoomEventHandler. */ + private transient PZoomEventHandler zoomEventHandler; + + private boolean paintingImmediately; + + /** Used to track whether the last paint operation was during an animation. */ + private boolean animatingOnLastPaint; + + /** The mouse listener that is registered for large scale mouse events. */ + private transient MouseListener mouseListener; + + /** Remembers the key processor. */ + private transient KeyEventPostProcessor keyEventPostProcessor; + + /** The mouse wheel listeners that's registered to receive wheel events. */ + private transient MouseWheelListener mouseWheelListener; + /** + * The mouse listener that is registered to receive small scale mouse events + * (like motion). + */ + private transient MouseMotionListener mouseMotionListener; + + private static final int ALL_BUTTONS_MASK = InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON2_DOWN_MASK + | InputEvent.BUTTON3_DOWN_MASK; + + /** + * Construct a canvas with the basic scene graph consisting of a root, + * camera, and layer. Zooming and panning are automatically installed. + */ + public PCanvas() { + CURRENT_ZCANVAS = this; + cursorStack = new PStack(); + setCamera(createDefaultCamera()); + setDefaultRenderQuality(PPaintContext.HIGH_QUALITY_RENDERING); + setAnimatingRenderQuality(PPaintContext.LOW_QUALITY_RENDERING); + setInteractingRenderQuality(PPaintContext.LOW_QUALITY_RENDERING); + setPanEventHandler(new PPanEventHandler()); + setZoomEventHandler(new PZoomEventHandler()); + setBackground(Color.WHITE); + setOpaque(true); + + addHierarchyListener(new HierarchyListener() { + public void hierarchyChanged(final HierarchyEvent e) { + if (e.getComponent() == PCanvas.this) { + if (getParent() == null) { + removeInputSources(); + } + else if (isEnabled()) { + installInputSources(); + } + } + } + }); + } + + /** + * Creates and returns a basic Scene Graph. + * + * @return a built PCamera scene + */ + protected PCamera createDefaultCamera() { + return PUtil.createBasicScenegraph(); + } + + // **************************************************************** + // Basic - Methods for accessing common piccolo nodes. + // **************************************************************** + + /** + * Get the pan event handler associated with this canvas. This event handler + * is set up to get events from the camera associated with this canvas by + * default. + * + * @return the current pan event handler, may be null + */ + public PPanEventHandler getPanEventHandler() { + return panEventHandler; + } + + /** + * Set the pan event handler associated with this canvas. + * + * @param handler the new zoom event handler + */ + public void setPanEventHandler(final PPanEventHandler handler) { + if (panEventHandler != null) { + removeInputEventListener(panEventHandler); + } + + panEventHandler = handler; + + if (panEventHandler != null) { + addInputEventListener(panEventHandler); + } + } + + /** + * Get the zoom event handler associated with this canvas. This event + * handler is set up to get events from the camera associated with this + * canvas by default. + * + * @return the current zoom event handler, may be null + */ + public PZoomEventHandler getZoomEventHandler() { + return zoomEventHandler; + } + + /** + * Set the zoom event handler associated with this canvas. + * + * @param handler the new zoom event handler + */ + public void setZoomEventHandler(final PZoomEventHandler handler) { + if (zoomEventHandler != null) { + removeInputEventListener(zoomEventHandler); + } + + zoomEventHandler = handler; + + if (zoomEventHandler != null) { + addInputEventListener(zoomEventHandler); + } + } + + /** + * Return the camera associated with this canvas. All input events from this + * canvas go through this camera. And this is the camera that paints this + * canvas. + * + * @return camera through which this PCanvas views the scene + */ + public PCamera getCamera() { + return camera; + } + + /** + * Set the camera associated with this canvas. All input events from this + * canvas go through this camera. And this is the camera that paints this + * canvas. + * + * @param newCamera the camera which this PCanvas should view the scene + */ + public void setCamera(final PCamera newCamera) { + if (camera != null) { + camera.setComponent(null); + } + + camera = newCamera; + + if (camera != null) { + camera.setComponent(this); + camera.setBounds(getBounds()); + } + } + + /** + * Return root for this canvas. + * + * @return the root PNode at the "bottom" of the scene + */ + public PRoot getRoot() { + return camera.getRoot(); + } + + /** + * Return layer for this canvas. + * + * @return the first layer attached to this camera + */ + public PLayer getLayer() { + return camera.getLayer(0); + } + + /** + * Add an input listener to the camera associated with this canvas. + * + * @param listener listener to register for event notifications + */ + public void addInputEventListener(final PInputEventListener listener) { + getCamera().addInputEventListener(listener); + } + + /** + * Remove an input listener to the camera associated with this canvas. + * + * @param listener listener to unregister from event notifications + */ + public void removeInputEventListener(final PInputEventListener listener) { + getCamera().removeInputEventListener(listener); + } + + // **************************************************************** + // Painting + // **************************************************************** + + /** + * Return true if this canvas has been marked as interacting, or whether + * it's root is interacting. If so the canvas will normally render at a + * lower quality that is faster. + * + * @return whether the canvas has been flagged as being interacted with + */ + public boolean getInteracting() { + return interacting > 0 || getRoot().getInteracting(); + } + + /** + * Return true if any activities that respond with true to the method + * isAnimating were run in the last PRoot.processInputs() loop. This values + * is used by this canvas to determine the render quality to use for the + * next paint. + * + * @return whether the PCanvas is currently being animated + */ + public boolean getAnimating() { + return getRoot().getActivityScheduler().getAnimating(); + } + + /** + * Set if this canvas is interacting. If so the canvas will normally render + * at a lower quality that is faster. Also repaints the canvas if the render + * quality should change. + * + * @param isInteracting whether the PCanvas should be considered interacting + */ + public void setInteracting(final boolean isInteracting) { + final boolean wasInteracting = getInteracting(); + + if (isInteracting) { + interacting++; + } + else { + interacting--; + } + + if (!getInteracting()) { // determine next render quality and repaint if + // it's greater then the old + // interacting render quality. + int nextRenderQuality = normalRenderQuality; + if (getAnimating()) { + nextRenderQuality = animatingRenderQuality; + } + if (nextRenderQuality > interactingRenderQuality) { + repaint(); + } + } + + final boolean newInteracting = getInteracting(); + + if (wasInteracting != newInteracting) { + firePropertyChange(PROPERTY_INTERACTING, wasInteracting, newInteracting); + } + } + + /** + * Set the render quality that should be used when rendering this canvas + * when it is not interacting or animating. The default value is + * PPaintContext. HIGH_QUALITY_RENDERING. + * + * @param defaultRenderQuality supports PPaintContext.HIGH_QUALITY_RENDERING + * or PPaintContext.LOW_QUALITY_RENDERING + */ + public void setDefaultRenderQuality(final int defaultRenderQuality) { + this.normalRenderQuality = defaultRenderQuality; + repaint(); + } + + /** + * Set the render quality that should be used when rendering this canvas + * when it is animating. The default value is + * PPaintContext.LOW_QUALITY_RENDERING. + * + * @param animatingRenderQuality supports + * PPaintContext.HIGH_QUALITY_RENDERING or + * PPaintContext.LOW_QUALITY_RENDERING + */ + public void setAnimatingRenderQuality(final int animatingRenderQuality) { + this.animatingRenderQuality = animatingRenderQuality; + if (getAnimating()) { + repaint(); + } + } + + /** + * Set the render quality that should be used when rendering this canvas + * when it is interacting. The default value is + * PPaintContext.LOW_QUALITY_RENDERING. + * + * @param interactingRenderQuality supports + * PPaintContext.HIGH_QUALITY_RENDERING or + * PPaintContext.LOW_QUALITY_RENDERING + */ + public void setInteractingRenderQuality(final int interactingRenderQuality) { + this.interactingRenderQuality = interactingRenderQuality; + if (getInteracting()) { + repaint(); + } + } + + /** + * Set the canvas cursor, and remember the previous cursor on the cursor + * stack. + * + * @param cursor the cursor to push onto the cursor stack + */ + public void pushCursor(final Cursor cursor) { + cursorStack.push(getCursor()); + setCursor(cursor); + } + + /** + * Pop the cursor on top of the cursorStack and set it as the canvas cursor. + */ + public void popCursor() { + if (!cursorStack.isEmpty()) { + setCursor((Cursor) cursorStack.pop()); + } + } + + // **************************************************************** + // Code to manage connection to Swing. There appears to be a bug in + // swing where it will occasionally send too many mouse pressed or mouse + // released events. Below we attempt to filter out those cases before + // they get delivered to the Piccolo framework. + // **************************************************************** + + /** + * Tracks whether button1 of the mouse is down. + */ + private boolean isButton1Pressed; + /** + * Tracks whether button2 of the mouse is down. + */ + private boolean isButton2Pressed; + /** + * Tracks whether button3 of the mouse is down. + */ + private boolean isButton3Pressed; + + /** + * Override setEnabled to install/remove canvas input sources as needed. + * + * @param enabled new enable status of the Pcanvas + */ + public void setEnabled(final boolean enabled) { + super.setEnabled(enabled); + + if (isEnabled() && getParent() != null) { + installInputSources(); + } + else { + removeInputSources(); + } + } + + /** + * This method installs mouse and key listeners on the canvas that forward + * those events to piccolo. + */ + protected void installInputSources() { + if (mouseListener == null) { + mouseListener = new MouseEventInputSource(); + addMouseListener(mouseListener); + } + + if (mouseMotionListener == null) { + mouseMotionListener = new MouseMotionInputSourceListener(); + addMouseMotionListener(mouseMotionListener); + } + + if (mouseWheelListener == null) { + mouseWheelListener = new MouseWheelInputSourceListener(); + addMouseWheelListener(mouseWheelListener); + } + + if (keyEventPostProcessor == null) { + keyEventPostProcessor = new KeyEventInputSourceListener(); + KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventPostProcessor(keyEventPostProcessor); + } + } + + /** + * This method removes mouse and key listeners on the canvas that forward + * those events to piccolo. + */ + protected void removeInputSources() { + removeMouseListener(mouseListener); + removeMouseMotionListener(mouseMotionListener); + removeMouseWheelListener(mouseWheelListener); + KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventPostProcessor(keyEventPostProcessor); + + mouseListener = null; + mouseMotionListener = null; + mouseWheelListener = null; + keyEventPostProcessor = null; + } + + /** + * Sends the given input event with the given type to the current + * InputManager. + * + * @param event event to dispatch + * @param type type of event being dispatched + */ + protected void sendInputEventToInputManager(final InputEvent event, final int type) { + getRoot().getDefaultInputManager().processEventFromCamera(event, type, getCamera()); + } + + /** + * Updates the bounds of the component and updates the camera accordingly. + * + * @param x left of bounds + * @param y top of bounds + * @param width width of bounds + * @param height height of bounds + */ + public void setBounds(final int x, final int y, final int width, final int height) { + camera.setBounds(camera.getX(), camera.getY(), width, height); + super.setBounds(x, y, width, height); + } + + /** + * {@inheritDoc} + */ + public void repaint(final PBounds bounds) { + PDebug.processRepaint(); + + bounds.expandNearestIntegerDimensions(); + bounds.inset(-1, -1); + + repaint((int) bounds.x, (int) bounds.y, (int) bounds.width, (int) bounds.height); + } + + private PBounds repaintBounds = new PBounds(); + + /** + * {@inheritDoc} + */ + public void paintComponent(final Graphics g) { + PDebug.startProcessingOutput(); + + final Graphics2D g2 = (Graphics2D) g.create(); + + // support for non-opaque canvases + // see + // http://groups.google.com/group/piccolo2d-dev/browse_thread/thread/134e2792d3a54cf + if (isOpaque()) { + g2.setColor(getBackground()); + g2.fillRect(0, 0, getWidth(), getHeight()); + } + + if (getAnimating()) { + repaintBounds.add(g2.getClipBounds()); + } + + // create new paint context and set render quality to lowest common + // denominator render quality. + final PPaintContext paintContext = new PPaintContext(g2); + if (getInteracting() || getAnimating()) { + if (interactingRenderQuality < animatingRenderQuality) { + paintContext.setRenderQuality(interactingRenderQuality); + } + else { + paintContext.setRenderQuality(animatingRenderQuality); + } + } + else { + paintContext.setRenderQuality(normalRenderQuality); + } + + camera.fullPaint(paintContext); + + // if switched state from animating to not animating invalidate the + // repaint bounds so that it will be drawn with the default instead of + // animating render quality. + if (!getAnimating() && animatingOnLastPaint) { + repaint(repaintBounds); + repaintBounds.reset(); + } + + animatingOnLastPaint = getAnimating(); + + PDebug.endProcessingOutput(g2); + } + + /** + * If not painting immediately, send paint notification to RepaintManager, + * otherwise does nothing. + */ + public void paintImmediately() { + if (paintingImmediately) { + return; + } + + paintingImmediately = true; + RepaintManager.currentManager(this).paintDirtyRegions(); + paintingImmediately = false; + } + + /** + * Helper for creating a timer. It's an extension point for subclasses to + * install their own timers. + * + * @param delay the number of milliseconds to wait before invoking the + * listener + * @param listener the listener to invoke after the delay + * + * @return the created Timer + */ + public Timer createTimer(final int delay, final ActionListener listener) { + return new Timer(delay, listener); + } + + /** + * Returns the quality to use when not animating or interacting. + * + * @since 1.3 + * @deprecated in favor or getNormalRenderQuality + * @return the render quality to use when not animating or interacting + */ + public int getDefaultRenderQuality() { + return normalRenderQuality; + } + + /** + * Returns the quality to use when not animating or interacting. + * + * @since 1.3 + * @return the render quality to use when not animating or interacting + */ + public int getNormalRenderQuality() { + return normalRenderQuality; + } + + /** + * Returns the quality to use when animating. + * + * @since 1.3 + * @return Returns the quality to use when animating + */ + public int getAnimatingRenderQuality() { + return animatingRenderQuality; + } + + /** + * Returns the quality to use when interacting. + * + * @since 1.3 + * @return Returns the quality to use when interacting + */ + public int getInteractingRenderQuality() { + return interactingRenderQuality; + } + + /** + * Returns the input event listeners registered to receive input events. + * + * @since 1.3 + * @return array or input event listeners + */ + public PInputEventListener[] getInputEventListeners() { + return camera.getInputEventListeners(); + } + + /** + * Prints the entire scene regardless of what the viewable area is. + * + * @param graphics Graphics context onto which to paint the scene for printing + */ + public void printAll(final Graphics graphics) { + if (!(graphics instanceof Graphics2D)) { + throw new IllegalArgumentException("Provided graphics context is not a Graphics2D object"); + } + + final Graphics2D g2 = (Graphics2D) graphics; + + final PBounds clippingRect = new PBounds(graphics.getClipBounds()); + clippingRect.expandNearestIntegerDimensions(); + + final PBounds originalCameraBounds = getCamera().getBounds(); + final PBounds layerBounds = getCamera().getUnionOfLayerFullBounds(); + getCamera().setBounds(layerBounds); + + final double clipRatio = clippingRect.getWidth() / clippingRect.getHeight(); + final double nodeRatio = ((double) getWidth()) / ((double) getHeight()); + final double scale; + if (nodeRatio <= clipRatio) { + scale = clippingRect.getHeight() / getCamera().getHeight(); + } + else { + scale = clippingRect.getWidth() / getCamera().getWidth(); + } + g2.scale(scale, scale); + g2.translate(-clippingRect.x, -clippingRect.y); + + final PPaintContext pc = new PPaintContext(g2); + pc.setRenderQuality(PPaintContext.HIGH_QUALITY_RENDERING); + getCamera().fullPaint(pc); + + getCamera().setBounds(originalCameraBounds); + } + + private final class MouseMotionInputSourceListener implements MouseMotionListener { + /** {@inheritDoc} */ + public void mouseDragged(final MouseEvent e) { + sendInputEventToInputManager(e, MouseEvent.MOUSE_DRAGGED); + } + + /** {@inheritDoc} */ + public void mouseMoved(final MouseEvent e) { + sendInputEventToInputManager(e, MouseEvent.MOUSE_MOVED); + } + } + + private final class MouseEventInputSource implements MouseListener { + /** {@inheritDoc} */ + public void mouseClicked(final MouseEvent e) { + sendInputEventToInputManager(e, MouseEvent.MOUSE_CLICKED); + } + + /** {@inheritDoc} */ + public void mouseEntered(final MouseEvent e) { + MouseEvent simulated = null; + + if (isAnyButtonDown(e)) { + simulated = buildRetypedMouseEvent(e, MouseEvent.MOUSE_DRAGGED); + } + else { + simulated = buildRetypedMouseEvent(e, MouseEvent.MOUSE_MOVED); + } + + sendInputEventToInputManager(e, MouseEvent.MOUSE_ENTERED); + sendInputEventToInputManager(simulated, simulated.getID()); + } + + /** {@inheritDoc} */ + public void mouseExited(final MouseEvent e) { + MouseEvent simulated = null; + + if (isAnyButtonDown(e)) { + simulated = buildRetypedMouseEvent(e, MouseEvent.MOUSE_DRAGGED); + } + else { + simulated = buildRetypedMouseEvent(e, MouseEvent.MOUSE_MOVED); + } + + sendInputEventToInputManager(simulated, simulated.getID()); + sendInputEventToInputManager(e, MouseEvent.MOUSE_EXITED); + } + + /** {@inheritDoc} */ + public void mousePressed(final MouseEvent rawEvent) { + requestFocus(); + + boolean shouldBalanceEvent = false; + + final MouseEvent event = copyButtonsFromModifiers(rawEvent, MouseEvent.MOUSE_PRESSED); + + switch (event.getButton()) { + case MouseEvent.BUTTON1: + if (isButton1Pressed) { + shouldBalanceEvent = true; + } + isButton1Pressed = true; + break; + + case MouseEvent.BUTTON2: + if (isButton2Pressed) { + shouldBalanceEvent = true; + } + isButton2Pressed = true; + break; + + case MouseEvent.BUTTON3: + if (isButton3Pressed) { + shouldBalanceEvent = true; + } + isButton3Pressed = true; + break; + default: + throw new RuntimeException("mousePressed without buttons specified"); + + } + + if (shouldBalanceEvent) { + sendRetypedMouseEventToInputManager(event, MouseEvent.MOUSE_RELEASED); + } + + sendInputEventToInputManager(event, MouseEvent.MOUSE_PRESSED); + } + + /** {@inheritDoc} */ + public void mouseReleased(final MouseEvent rawEvent) { + boolean shouldBalanceEvent = false; + + final MouseEvent event = copyButtonsFromModifiers(rawEvent, MouseEvent.MOUSE_RELEASED); + + switch (event.getButton()) { + case MouseEvent.BUTTON1: + if (!isButton1Pressed) { + shouldBalanceEvent = true; + } + isButton1Pressed = false; + break; + + case MouseEvent.BUTTON2: + if (!isButton2Pressed) { + shouldBalanceEvent = true; + } + isButton2Pressed = false; + break; + + case MouseEvent.BUTTON3: + if (!isButton3Pressed) { + shouldBalanceEvent = true; + } + isButton3Pressed = false; + break; + default: + throw new RuntimeException("mouseReleased without buttons specified"); + } + + if (shouldBalanceEvent) { + sendRetypedMouseEventToInputManager(event, MouseEvent.MOUSE_PRESSED); + } + + sendInputEventToInputManager(event, MouseEvent.MOUSE_RELEASED); + } + + private MouseEvent copyButtonsFromModifiers(final MouseEvent rawEvent, final int eventType) { + if (rawEvent.getButton() != MouseEvent.NOBUTTON) { + return rawEvent; + } + + int newButton = 0; + + if (hasButtonModifier(rawEvent, InputEvent.BUTTON1_MASK)) { + newButton = MouseEvent.BUTTON1; + } + else if (hasButtonModifier(rawEvent, InputEvent.BUTTON2_MASK)) { + newButton = MouseEvent.BUTTON2; + } + else if (hasButtonModifier(rawEvent, InputEvent.BUTTON3_MASK)) { + newButton = MouseEvent.BUTTON3; + } + + return buildModifiedMouseEvent(rawEvent, eventType, newButton); + } + + private boolean hasButtonModifier(final MouseEvent event, final int buttonMask) { + return (event.getModifiers() & buttonMask) == buttonMask; + } + + public MouseEvent buildRetypedMouseEvent(final MouseEvent e, final int newType) { + return buildModifiedMouseEvent(e, newType, e.getButton()); + } + + public MouseEvent buildModifiedMouseEvent(final MouseEvent e, final int newType, final int newButton) { + return new MouseEvent((Component) e.getSource(), newType, e.getWhen(), e.getModifiers(), e.getX(), + e.getY(), e.getClickCount(), e.isPopupTrigger(), newButton); + } + + private void sendRetypedMouseEventToInputManager(final MouseEvent e, final int newType) { + final MouseEvent retypedEvent = buildRetypedMouseEvent(e, newType); + sendInputEventToInputManager(retypedEvent, newType); + } + } + + private boolean isAnyButtonDown(final MouseEvent e) { + return (e.getModifiersEx() & ALL_BUTTONS_MASK) != 0; + } + + /** + * Class responsible for sending key events to the the InputManager. + */ + private final class KeyEventInputSourceListener implements KeyEventPostProcessor { + /** {@inheritDoc} */ + public boolean postProcessKeyEvent(final KeyEvent keyEvent) { + Component owner = FocusManager.getCurrentManager().getFocusOwner(); + while (owner != null) { + if (owner == PCanvas.this) { + sendInputEventToInputManager(keyEvent, keyEvent.getID()); + return true; + } + owner = owner.getParent(); + } + return false; + } + } + + /** + * Class responsible for sending mouse events to the the InputManager. + */ + private final class MouseWheelInputSourceListener implements MouseWheelListener { + /** {@inheritDoc} */ + public void mouseWheelMoved(final MouseWheelEvent e) { + sendInputEventToInputManager(e, e.getScrollType()); + if (!e.isConsumed() && getParent() != null) { + getParent().dispatchEvent(e); + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/edu/umd/cs/piccolo/PComponent.java b/src/main/java/edu/umd/cs/piccolo/PComponent.java new file mode 100644 index 0000000..e740edf --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/PComponent.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo; + +import java.awt.Cursor; + +import edu.umd.cs.piccolo.util.PBounds; + +/** + * Interface that a component needs to implement if it wants to act as a Piccolo + * canvas. + * + * @version 1.0 + * @author Lance Good + */ +public interface PComponent { + + /** + * Called to notify PComponent that given bounds need repainting. + * + * @param bounds bounds needing repaint + */ + void repaint(PBounds bounds); + + /** + * Sends a repaint notification the repaint manager if PComponent is not + * already painting immediately. + */ + void paintImmediately(); + + /** + * Pushes the given cursor onto the cursor stack and sets the current cursor + * to the one provided. + * + * @param cursor The cursor to set as the current one and push + */ + void pushCursor(Cursor cursor); + + /** + * Pops the topmost cursor from the stack and sets it as the current one. + */ + void popCursor(); + + /** + * Sets whether the component is currently being interacted with. + * + * @param interacting whether the component is currently being interacted + * with + */ + void setInteracting(boolean interacting); +} diff --git a/src/main/java/edu/umd/cs/piccolo/PInputManager.java b/src/main/java/edu/umd/cs/piccolo/PInputManager.java new file mode 100644 index 0000000..7620849 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/PInputManager.java @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo; + +import java.awt.event.FocusEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.geom.Point2D; + +import edu.umd.cs.piccolo.event.PBasicInputEventHandler; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.event.PInputEventListener; +import edu.umd.cs.piccolo.util.PPickPath; + +/** + * PInputManager is responsible for dispatching PInputEvents to node's + * event listeners. Events are dispatched from PRoot's processInputs method. + *
+ * + * @see edu.umd.cs.piccolo.event.PInputEvent + * @see PRoot + * @version 1.0 + * @author Jesse Grosjean + */ +public class PInputManager extends PBasicInputEventHandler implements PRoot.InputSource { + + /** Records the last known mouse position on the canvas. */ + private final Point2D lastCanvasPosition; + + /** Records the current known mouse position on the canvas. */ + private final Point2D currentCanvasPosition; + + /** The next InputEvent that needs to be processed. */ + private InputEvent nextInput; + + /** The type of the next InputEvent that needs to be processed. */ + private int nextType; + + /** The Input Source the next event to process came from. */ + private PCamera nextInputSource; + + /** The current mouse focus. */ + private PPickPath mouseFocus; + + /** The previous mouse focus. */ + private PPickPath previousMouseFocus; + + /** Tracks where the mouse is right now on the canvas. */ + private PPickPath mouseOver; + + /** Tracks the previous location of the mouse on the canvas. */ + private PPickPath previousMouseOver; + + /** Tracks the input event listener that should receive keyboard events. */ + private PInputEventListener keyboardFocus; + + /** Tracks the number mouse buttons currently pressed. */ + private int buttonsPressed; + + /** + * Creates a PInputManager and sets positions (last, current) to the origin + * (0,0). + */ + public PInputManager() { + lastCanvasPosition = new Point2D.Double(); + currentCanvasPosition = new Point2D.Double(); + } + + /** + * Return the node that currently has the keyboard focus. This node receives + * the key events. + * + * @return the current keyboard focus + */ + public PInputEventListener getKeyboardFocus() { + return keyboardFocus; + } + + /** + * Set the node that should receive key events. + * + * @param eventHandler sets the keyboard event focus, may be null + */ + public void setKeyboardFocus(final PInputEventListener eventHandler) { + final PInputEvent focusEvent = new PInputEvent(this, null); + + if (keyboardFocus != null) { + dispatchEventToListener(focusEvent, FocusEvent.FOCUS_LOST, keyboardFocus); + } + + keyboardFocus = eventHandler; + + if (keyboardFocus != null) { + dispatchEventToListener(focusEvent, FocusEvent.FOCUS_GAINED, keyboardFocus); + } + } + + /** + * Return the current Pick Path under the mouse focus. This will return the + * path that received the current mouse pressed event, or null if the mouse + * is not pressed. The mouse focus gets mouse dragged events even what the + * mouse is not over the mouse focus. + * + * @return the current Pick Path under the mouse focus + */ + public PPickPath getMouseFocus() { + return mouseFocus; + } + + /** + * Sets the current Pick Path under the mouse focus. The mouse focus gets + * mouse dragged events even when the mouse is not over the mouse focus. + * + * @param path the new mouse focus + */ + public void setMouseFocus(final PPickPath path) { + previousMouseFocus = mouseFocus; + mouseFocus = path; + } + + /** + * Return the node the the mouse is currently over. + * + * @return the path over which the mouse currently is + */ + public PPickPath getMouseOver() { + return mouseOver; + } + + /** + * Records the path which is directly below the mouse. + * + * @param path path over which the mouse has been moved + */ + public void setMouseOver(final PPickPath path) { + mouseOver = path; + } + + /** + * Returns the position on the Canvas of the last event. + * + * @return position of last canvas event + */ + public Point2D getLastCanvasPosition() { + return lastCanvasPosition; + } + + /** + * Returns the position of the current canvas event. + * + * @return position of current canvas event + */ + public Point2D getCurrentCanvasPosition() { + return currentCanvasPosition; + } + + // **************************************************************** + // Event Handling - Methods for handling events + // + // The dispatch manager updates the focus nodes based on the + // incoming events, and dispatches those events to the appropriate + // focus nodes. + // **************************************************************** + + /** {@inheritDoc} */ + public void keyPressed(final PInputEvent event) { + dispatchEventToListener(event, KeyEvent.KEY_PRESSED, keyboardFocus); + } + + /** {@inheritDoc} */ + public void keyReleased(final PInputEvent event) { + dispatchEventToListener(event, KeyEvent.KEY_RELEASED, keyboardFocus); + } + + /** {@inheritDoc} */ + public void keyTyped(final PInputEvent event) { + dispatchEventToListener(event, KeyEvent.KEY_TYPED, keyboardFocus); + } + + /** {@inheritDoc} */ + public void mouseClicked(final PInputEvent event) { + dispatchEventToListener(event, MouseEvent.MOUSE_CLICKED, previousMouseFocus); + } + + /** {@inheritDoc} */ + public void mouseWheelRotated(final PInputEvent event) { + setMouseFocus(getMouseOver()); + dispatchEventToListener(event, MouseWheelEvent.WHEEL_UNIT_SCROLL, mouseOver); + } + + /** {@inheritDoc} */ + public void mouseWheelRotatedByBlock(final PInputEvent event) { + setMouseFocus(getMouseOver()); + dispatchEventToListener(event, MouseWheelEvent.WHEEL_BLOCK_SCROLL, mouseOver); + } + + /** {@inheritDoc} */ + public void mouseDragged(final PInputEvent event) { + checkForMouseEnteredAndExited(event); + dispatchEventToListener(event, MouseEvent.MOUSE_DRAGGED, mouseFocus); + } + + /** {@inheritDoc} */ + public void mouseEntered(final PInputEvent event) { + dispatchEventToListener(event, MouseEvent.MOUSE_ENTERED, mouseOver); + } + + /** {@inheritDoc} */ + public void mouseExited(final PInputEvent event) { + dispatchEventToListener(event, MouseEvent.MOUSE_EXITED, previousMouseOver); + } + + /** {@inheritDoc} */ + public void mouseMoved(final PInputEvent event) { + checkForMouseEnteredAndExited(event); + dispatchEventToListener(event, MouseEvent.MOUSE_MOVED, mouseOver); + } + + /** {@inheritDoc} */ + public void mousePressed(final PInputEvent event) { + if (buttonsPressed == 0) { + setMouseFocus(getMouseOver()); + } + buttonsPressed++; + dispatchEventToListener(event, MouseEvent.MOUSE_PRESSED, mouseFocus); + if (buttonsPressed < 1 || buttonsPressed > 3) { + System.err.println("invalid pressedCount on mouse pressed: " + buttonsPressed); + } + } + + /** {@inheritDoc} */ + public void mouseReleased(final PInputEvent event) { + buttonsPressed--; + checkForMouseEnteredAndExited(event); + dispatchEventToListener(event, MouseEvent.MOUSE_RELEASED, mouseFocus); + if (buttonsPressed == 0) { + setMouseFocus(null); + } + if (buttonsPressed < 0 || buttonsPressed > 2) { + System.err.println("invalid pressedCount on mouse released: " + buttonsPressed); + } + } + + /** + * Fires events whenever the mouse moves from PNode to PNode. + * + * @param event to check to see if the top node has changed. + */ + protected void checkForMouseEnteredAndExited(final PInputEvent event) { + final PNode currentNode = getPickedNode(mouseOver); + final PNode previousNode = getPickedNode(previousMouseOver); + + if (currentNode != previousNode) { + dispatchEventToListener(event, MouseEvent.MOUSE_EXITED, previousMouseOver); + dispatchEventToListener(event, MouseEvent.MOUSE_ENTERED, mouseOver); + previousMouseOver = mouseOver; + } + } + + /** + * Returns picked node on pickPath if pickPath is not null, or null. + * + * @param pickPath from which to extract picked node + * + * @return the picked node or null if pickPath is null + */ + private PNode getPickedNode(final PPickPath pickPath) { + if (pickPath == null) { + return null; + } + else { + return pickPath.getPickedNode(); + } + } + + // **************************************************************** + // Event Dispatch. + // **************************************************************** + /** {@inheritDoc} */ + public void processInput() { + if (nextInput == null) { + return; + } + + final PInputEvent e = new PInputEvent(this, nextInput); + + Point2D newCurrentCanvasPosition = null; + Point2D newLastCanvasPosition = null; + + if (e.isMouseEvent()) { + if (e.isMouseEnteredOrMouseExited()) { + final PPickPath aPickPath = nextInputSource.pick(((MouseEvent) nextInput).getX(), + ((MouseEvent) nextInput).getY(), 1); + setMouseOver(aPickPath); + previousMouseOver = aPickPath; + newCurrentCanvasPosition = (Point2D) currentCanvasPosition.clone(); + newLastCanvasPosition = (Point2D) lastCanvasPosition.clone(); + } + else { + lastCanvasPosition.setLocation(currentCanvasPosition); + currentCanvasPosition.setLocation(((MouseEvent) nextInput).getX(), ((MouseEvent) nextInput).getY()); + final PPickPath aPickPath = nextInputSource.pick(currentCanvasPosition.getX(), currentCanvasPosition + .getY(), 1); + setMouseOver(aPickPath); + } + } + + nextInput = null; + nextInputSource = null; + + processEvent(e, nextType); + + if (newCurrentCanvasPosition != null && newLastCanvasPosition != null) { + currentCanvasPosition.setLocation(newCurrentCanvasPosition); + lastCanvasPosition.setLocation(newLastCanvasPosition); + } + } + + /** + * Flags the given event as needing to be processed. + * + * @param event the event to be processed + * @param type type of event to be processed + * @param camera camera from which the event was dispatched + */ + public void processEventFromCamera(final InputEvent event, final int type, final PCamera camera) { + // queue input + nextInput = event; + nextType = type; + nextInputSource = camera; + + // tell root to process queued inputs + camera.getRoot().processInputs(); + } + + /** + * Dispatches the given event to the listener, or does nothing if listener + * is null. + * + * @param event event to be dispatched + * @param type type of event to dispatch + * @param listener target of dispatch + */ + private void dispatchEventToListener(final PInputEvent event, final int type, final PInputEventListener listener) { + if (listener != null) { + // clear the handled bit since the same event object is used to send + // multiple events such as mouseEntered/mouseExited and mouseMove. + event.setHandled(false); + listener.processEvent(event, type); + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/PLayer.java b/src/main/java/edu/umd/cs/piccolo/PLayer.java new file mode 100644 index 0000000..98befdc --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/PLayer.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; + +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PObjectOutputStream; + +/** + * PLayer is a node that can be viewed directly by multiple camera nodes. + * Generally child nodes are added to a layer to give the viewing cameras + * something to look at. + *
+ * A single layer node may be viewed through multiple cameras with each camera + * using its own view transform. This means that any node (since layers can have + * children) may be visible through multiple cameras at the same time. + *
+ * + * @see PCamera + * @see edu.umd.cs.piccolo.event.PInputEvent + * @see edu.umd.cs.piccolo.util.PPickPath + * @version 1.0 + * @author Jesse Grosjean + */ +public class PLayer extends PNode { + /** + * Allows for future serialization code to understand versioned binary + * formats. + */ + private static final long serialVersionUID = 1L; + + /** + * The property name that identifies a change in the set of this layer's + * cameras (see {@link #getCamera getCamera}, {@link #getCameraCount + * getCameraCount}, {@link #getCamerasReference getCamerasReference}). In + * any property change event the new value will be a reference to the list + * of cameras, but old value will always be null. + */ + public static final String PROPERTY_CAMERAS = "cameras"; + + /** + * The property code that identifies a change in the set of this layer's + * cameras (see {@link #getCamera getCamera}, {@link #getCameraCount + * getCameraCount}, {@link #getCamerasReference getCamerasReference}). In + * any property change event the new value will be a reference to the list + * of cameras, but old value will always be null. + */ + public static final int PROPERTY_CODE_CAMERAS = 1 << 13; + + /** + * Cameras which are registered as viewers of this PLayer. + */ + private transient List cameras; + + /** + * Creates a PLayer without any cameras attached to it. + */ + public PLayer() { + super(); + cameras = new ArrayList(); + } + + // **************************************************************** + // Cameras - Maintain the list of cameras that are viewing this + // layer. + // **************************************************************** + + /** + * Get the list of cameras viewing this layer. + * + * @return direct reference to registered cameras + */ + public List getCamerasReference() { + return cameras; + } + + /** + * Get the number of cameras viewing this layer. + * + * @return the number of cameras attached to this layer + */ + public int getCameraCount() { + if (cameras == null) { + return 0; + } + return cameras.size(); + } + + /** + * Get the camera in this layer's camera list at the specified index. + * + * @param index index of camera to fetch + * @return camera at the given index + */ + public PCamera getCamera(final int index) { + return (PCamera) cameras.get(index); + } + + /** + * Add a camera to this layer's camera list. This method it called + * automatically when a layer is added to a camera. + * + * @param camera the camera to add to this layer + */ + public void addCamera(final PCamera camera) { + addCamera(cameras.size(), camera); + } + + /** + * Add a camera to this layer's camera list at the specified index. This + * method it called automatically when a layer is added to a camera. + * + * @param index index at which the camera should be inserted + * @param camera Camera to add to layer + */ + public void addCamera(final int index, final PCamera camera) { + cameras.add(index, camera); + invalidatePaint(); + firePropertyChange(PROPERTY_CODE_CAMERAS, PROPERTY_CAMERAS, null, cameras); + } + + /** + * Remove the camera from this layer's camera list. + * + * @param camera the camera to remove from the layer, does nothing if not + * found + * @return camera that was passed in + */ + public PCamera removeCamera(final PCamera camera) { + if (cameras.remove(camera)) { + invalidatePaint(); + firePropertyChange(PROPERTY_CODE_CAMERAS, PROPERTY_CAMERAS, null, cameras); + } + return camera; + } + + /** + * Remove the camera at the given index from this layer's camera list. + * + * @param index the index of the camera we wish to remove + * + * @return camera that was removed + */ + public PCamera removeCamera(final int index) { + final PCamera result = (PCamera) cameras.remove(index); + invalidatePaint(); + firePropertyChange(PROPERTY_CODE_CAMERAS, PROPERTY_CAMERAS, null, cameras); + return result; + } + + // **************************************************************** + // Camera Repaint Notifications - Layer nodes must forward their + // repaints to each camera that is viewing them so that the camera + // views will also get repainted. + // **************************************************************** + + /** + * Override repaints and forward them to the cameras that are viewing this + * layer. + * + * @param localBounds bounds flagged as needing repainting + * @param repaintSource the source of the repaint notification + */ + public void repaintFrom(final PBounds localBounds, final PNode repaintSource) { + if (repaintSource != this) { + localToParent(localBounds); + } + + notifyCameras(localBounds); + + if (getParent() != null) { + getParent().repaintFrom(localBounds, repaintSource); + } + } + + /** + * Dispatches repaint notification to all registered cameras. + * + * @param parentBounds bounds needing repainting in parent coordinate system + */ + protected void notifyCameras(final PBounds parentBounds) { + final int count = getCameraCount(); + for (int i = 0; i < count; i++) { + final PCamera each = (PCamera) cameras.get(i); + each.repaintFromLayer(parentBounds, this); + } + } + + // **************************************************************** + // Serialization - Layers conditionally serialize their cameras. + // This means that only the camera references that were unconditionally + // (using writeObject) serialized by someone else will be restored + // when the layer is unserialized. + // **************************************************************** + + /** + * Write this layer and all its children out to the given stream. Note that + * the layer writes out any cameras that are viewing it conditionally, so + * they will only get written out if someone else writes them + * unconditionally. + * + * @param out object to which the layer should be streamed + * @throws IOException may occur while serializing to stream + */ + private void writeObject(final ObjectOutputStream out) throws IOException { + if (!(out instanceof PObjectOutputStream)) { + throw new RuntimeException("May not serialize PLayer to a non PObjectOutputStream"); + } + out.defaultWriteObject(); + + final int count = getCameraCount(); + for (int i = 0; i < count; i++) { + ((PObjectOutputStream) out).writeConditionalObject(cameras.get(i)); + } + + out.writeObject(Boolean.FALSE); + } + + /** + * Deserializes PLayer from the provided ObjectInputStream. + * + * @param in stream from which PLayer should be read + * + * @throws IOException since it involves quite a bit of IO + * @throws ClassNotFoundException may occur is serialized stream has been + * renamed after serialization + */ + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + + cameras = new ArrayList(); + + while (true) { + final Object each = in.readObject(); + if (each != null) { + if (each.equals(Boolean.FALSE)) { + break; + } + else { + cameras.add(each); + } + } + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/PNode.java b/src/main/java/edu/umd/cs/piccolo/PNode.java new file mode 100644 index 0000000..117e44b --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/PNode.java @@ -0,0 +1,3769 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsEnvironment; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Transparency; +import java.awt.geom.AffineTransform; +import java.awt.geom.Dimension2D; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.print.Book; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import javax.swing.event.EventListenerList; +import javax.swing.event.SwingPropertyChangeSupport; +import javax.swing.text.MutableAttributeSet; +import javax.swing.text.SimpleAttributeSet; + +import edu.umd.cs.piccolo.activities.PActivity; +import edu.umd.cs.piccolo.activities.PColorActivity; +import edu.umd.cs.piccolo.activities.PInterpolatingActivity; +import edu.umd.cs.piccolo.activities.PTransformActivity; +import edu.umd.cs.piccolo.event.PInputEventListener; +import edu.umd.cs.piccolo.util.PAffineTransform; +import edu.umd.cs.piccolo.util.PAffineTransformException; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PNodeFilter; +import edu.umd.cs.piccolo.util.PObjectOutputStream; +import edu.umd.cs.piccolo.util.PPaintContext; +import edu.umd.cs.piccolo.util.PPickPath; +import edu.umd.cs.piccolo.util.PUtil; + +/** + * PNode is the central abstraction in Piccolo. All objects that are + * visible on the screen are instances of the node class. All nodes may have + * other "child" nodes added to them. + *
+ * See edu.umd.piccolo.examples.NodeExample.java for demonstrations of how nodes + * can be used and how new types of nodes can be created. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PNode implements Cloneable, Serializable, Printable { + /** + * The minimum difference in transparency required before the transparency + * is allowed to change. Done for efficiency reasons. I doubt very much that + * the human eye could tell the difference between 0.01 and 0.02 + * transparency. + */ + private static final float TRANSPARENCY_RESOLUTION = 0.01f; + + /** + * Allows for future serialization code to understand versioned binary + * formats. + */ + private static final long serialVersionUID = 1L; + + /** + * The property name that identifies a change in this node's client + * propertie (see {@link #getClientProperty getClientProperty}). In an + * property change event the new value will be a reference to the map of + * client properties but old value will always be null. + */ + public static final String PROPERTY_CLIENT_PROPERTIES = "clientProperties"; + + /** + * The property code that identifies a change in this node's client + * propertie (see {@link #getClientProperty getClientProperty}). In an + * property change event the new value will be a reference to the map of + * client properties but old value will always be null. + */ + public static final int PROPERTY_CODE_CLIENT_PROPERTIES = 1 << 0; + + /** + * The property name that identifies a change of this node's bounds (see + * {@link #getBounds getBounds}, {@link #getBoundsReference + * getBoundsReference}). In any property change event the new value will be + * a reference to this node's bounds, but old value will always be null. + */ + public static final String PROPERTY_BOUNDS = "bounds"; + + /** + * The property code that identifies a change of this node's bounds (see + * {@link #getBounds getBounds}, {@link #getBoundsReference + * getBoundsReference}). In any property change event the new value will be + * a reference to this node's bounds, but old value will always be null. + */ + public static final int PROPERTY_CODE_BOUNDS = 1 << 1; + + /** + * The property name that identifies a change of this node's full bounds + * (see {@link #getFullBounds getFullBounds}, + * {@link #getFullBoundsReference getFullBoundsReference}). In any property + * change event the new value will be a reference to this node's full bounds + * cache, but old value will always be null. + */ + public static final String PROPERTY_FULL_BOUNDS = "fullBounds"; + + /** + * The property code that identifies a change of this node's full bounds + * (see {@link #getFullBounds getFullBounds}, + * {@link #getFullBoundsReference getFullBoundsReference}). In any property + * change event the new value will be a reference to this node's full bounds + * cache, but old value will always be null. + */ + public static final int PROPERTY_CODE_FULL_BOUNDS = 1 << 2; + + /** + * The property name that identifies a change of this node's transform (see + * {@link #getTransform getTransform}, {@link #getTransformReference + * getTransformReference}). In any property change event the new value will + * be a reference to this node's transform, but old value will always be + * null. + */ + public static final String PROPERTY_TRANSFORM = "transform"; + + /** + * The property code that identifies a change of this node's transform (see + * {@link #getTransform getTransform}, {@link #getTransformReference + * getTransformReference}). In any property change event the new value will + * be a reference to this node's transform, but old value will always be + * null. + */ + public static final int PROPERTY_CODE_TRANSFORM = 1 << 3; + + /** + * The property name that identifies a change of this node's visibility (see + * {@link #getVisible getVisible}). Both old value and new value will be + * null in any property change event. + */ + public static final String PROPERTY_VISIBLE = "visible"; + + /** + * The property code that identifies a change of this node's visibility (see + * {@link #getVisible getVisible}). Both old value and new value will be + * null in any property change event. + */ + public static final int PROPERTY_CODE_VISIBLE = 1 << 4; + + /** + * The property name that identifies a change of this node's paint (see + * {@link #getPaint getPaint}). Both old value and new value will be set + * correctly in any property change event. + */ + public static final String PROPERTY_PAINT = "paint"; + + /** + * The property code that identifies a change of this node's paint (see + * {@link #getPaint getPaint}). Both old value and new value will be set + * correctly in any property change event. + */ + public static final int PROPERTY_CODE_PAINT = 1 << 5; + + /** + * The property name that identifies a change of this node's transparency + * (see {@link #getTransparency getTransparency}). Both old value and new + * value will be null in any property change event. + */ + public static final String PROPERTY_TRANSPARENCY = "transparency"; + + /** + * The property code that identifies a change of this node's transparency + * (see {@link #getTransparency getTransparency}). Both old value and new + * value will be null in any property change event. + */ + public static final int PROPERTY_CODE_TRANSPARENCY = 1 << 6; + + /** + * The property name that identifies a change of this node's pickable status + * (see {@link #getPickable getPickable}). Both old value and new value will + * be null in any property change event. + */ + public static final String PROPERTY_PICKABLE = "pickable"; + /** + * The property code that identifies a change of this node's pickable status + * (see {@link #getPickable getPickable}). Both old value and new value will + * be null in any property change event. + */ + public static final int PROPERTY_CODE_PICKABLE = 1 << 7; + + /** + * The property name that identifies a change of this node's children + * pickable status (see {@link #getChildrenPickable getChildrenPickable}). + * Both old value and new value will be null in any property change event. + */ + public static final String PROPERTY_CHILDREN_PICKABLE = "childrenPickable"; + + /** + * The property code that identifies a change of this node's children + * pickable status (see {@link #getChildrenPickable getChildrenPickable}). + * Both old value and new value will be null in any property change event. + */ + public static final int PROPERTY_CODE_CHILDREN_PICKABLE = 1 << 8; + + /** + * The property name that identifies a change in the set of this node's + * direct children (see {@link #getChildrenReference getChildrenReference}, + * {@link #getChildrenIterator getChildrenIterator}). In any property change + * event the new value will be a reference to this node's children, but old + * value will always be null. + */ + public static final String PROPERTY_CHILDREN = "children"; + + /** + * The property code that identifies a change in the set of this node's + * direct children (see {@link #getChildrenReference getChildrenReference}, + * {@link #getChildrenIterator getChildrenIterator}). In any property change + * event the new value will be a reference to this node's children, but old + * value will always be null. + */ + public static final int PROPERTY_CODE_CHILDREN = 1 << 9; + + /** + * The property name that identifies a change of this node's parent (see + * {@link #getParent getParent}). Both old value and new value will be set + * correctly in any property change event. + */ + public static final String PROPERTY_PARENT = "parent"; + + /** + * The property code that identifies a change of this node's parent (see + * {@link #getParent getParent}). Both old value and new value will be set + * correctly in any property change event. + */ + public static final int PROPERTY_CODE_PARENT = 1 << 10; + + /** Is an optimization for use during repaints. */ + private static final PBounds TEMP_REPAINT_BOUNDS = new PBounds(); + + /** The single scene graph delegate that receives low level node events. */ + public static PSceneGraphDelegate SCENE_GRAPH_DELEGATE = null; + + /** Tracks the parent of this node, may be null. */ + private transient PNode parent; + + /** Tracks all immediate child nodes. */ + private List children; + + /** Bounds of the PNode. */ + private final PBounds bounds; + + /** Transform that applies to this node in relation to its parent. */ + private PAffineTransform transform; + + /** The paint to use for the background of this node. */ + private Paint paint; + + /** + * How Opaque this node should be 1f = fully opaque, 0f = completely + * transparent. + */ + private float transparency; + + /** A modifiable set of client properties. */ + private MutableAttributeSet clientProperties; + + /** + * An optimization that remembers the full bounds of a node rather than + * computing it every time. + */ + private PBounds fullBoundsCache; + + /** + * Mask used when deciding whether to bubble up property change events to + * parents. + */ + private int propertyChangeParentMask = 0; + + /** Used to handle property change listeners. */ + private transient SwingPropertyChangeSupport changeSupport; + + /** List of event listeners. */ + private transient EventListenerList listenerList; + + /** Whether this node is pickable or not. */ + private boolean pickable; + + /** + * Whether to stop processing pick at this node and not bother drilling down + * into children. + */ + private boolean childrenPickable; + + /** Whether this node will be rendered. */ + private boolean visible; + + private boolean childBoundsVolatile; + + /** Whether this node needs to be repainted. */ + private boolean paintInvalid; + + /** Whether children need to be repainted. */ + private boolean childPaintInvalid; + + /** Whether this node's bounds have changed, and so needs to be relaid out. */ + private boolean boundsChanged; + + /** Whether this node's full bounds need to be recomputed. */ + private boolean fullBoundsInvalid; + + /** Whether this node's child bounds need to be recomputed. */ + private boolean childBoundsInvalid; + + private boolean occluded; + + /** Stores the name associated to this node. */ + private String name; + + /** + * toImage fill strategy that stretches the node be as large as possible + * while still retaining its aspect ratio. + * + * @since 1.3 + */ + public static final int FILL_STRATEGY_ASPECT_FIT = 1; + + /** + * toImage fill strategy that stretches the node be large enough to cover + * the image, and centers it. + * + * @since 1.3 + */ + public static final int FILL_STRATEGY_ASPECT_COVER = 2; + + /** + * toImage fill strategy that stretches the node to be exactly the + * dimensions of the image. Will result in distortion if the aspect ratios + * are different. + * + * @since 1.3 + */ + public static final int FILL_STRATEGY_EXACT_FIT = 4; + + /** + * Creates a new PNode with the given name. + * + * @since 1.3 + * @param newName name to assign to node + */ + public PNode(final String newName) { + this(); + setName(newName); + } + + /** + * Constructs a new PNode. + *
+ * By default a node's paint is null, and bounds are empty. These values
+ * must be set for the node to show up on the screen once it's added to a
+ * scene graph.
+ */
+ public PNode() {
+ bounds = new PBounds();
+ fullBoundsCache = new PBounds();
+ transparency = 1.0f;
+ pickable = true;
+ childrenPickable = true;
+ visible = true;
+ }
+
+ // ****************************************************************
+ // Animation - Methods to animate this node.
+ //
+ // Note that animation is implemented by activities (PActivity),
+ // so if you need more control over your animation look at the
+ // activities package. Each animate method creates an animation that
+ // will animate the node from its current state to the new state
+ // specified over the given duration. These methods will try to
+ // automatically schedule the new activity, but if the node does not
+ // descend from the root node when the method is called then the
+ // activity will not be scheduled and you must schedule it manually.
+ // ****************************************************************
+
+ /**
+ * Animate this node's bounds from their current location when the activity
+ * starts to the specified bounds. If this node descends from the root then
+ * the activity will be scheduled, else the returned activity should be
+ * scheduled manually. If two different transform activities are scheduled
+ * for the same node at the same time, they will both be applied to the
+ * node, but the last one scheduled will be applied last on each frame, so
+ * it will appear to have replaced the original. Generally you will not want
+ * to do that. Note this method animates the node's bounds, but does not
+ * change the node's transform. Use animateTransformToBounds() to animate
+ * the node's transform instead.
+ *
+ * @param x left of target bounds
+ * @param y top of target bounds
+ * @param width width of target bounds
+ * @param height height of target bounds
+ * @param duration amount of time that the animation should take
+ * @return the newly scheduled activity
+ */
+ public PInterpolatingActivity animateToBounds(final double x, final double y, final double width,
+ final double height, final long duration) {
+ if (duration == 0) {
+ setBounds(x, y, width, height);
+ return null;
+ }
+
+ final PBounds dst = new PBounds(x, y, width, height);
+
+ final PInterpolatingActivity interpolatingActivity = new PInterpolatingActivity(duration,
+ PUtil.DEFAULT_ACTIVITY_STEP_RATE) {
+ private PBounds src;
+
+ protected void activityStarted() {
+ src = getBounds();
+ startResizeBounds();
+ super.activityStarted();
+ }
+
+ public void setRelativeTargetValue(final float zeroToOne) {
+ PNode.this.setBounds(src.x + zeroToOne * (dst.x - src.x), src.y + zeroToOne * (dst.y - src.y),
+ src.width + zeroToOne * (dst.width - src.width), src.height + zeroToOne
+ * (dst.height - src.height));
+ }
+
+ protected void activityFinished() {
+ super.activityFinished();
+ endResizeBounds();
+ }
+ };
+
+ addActivity(interpolatingActivity);
+ return interpolatingActivity;
+ }
+
+ /**
+ * Animate this node from it's current transform when the activity starts a
+ * new transform that will fit the node into the given bounds. If this node
+ * descends from the root then the activity will be scheduled, else the
+ * returned activity should be scheduled manually. If two different
+ * transform activities are scheduled for the same node at the same time,
+ * they will both be applied to the node, but the last one scheduled will be
+ * applied last on each frame, so it will appear to have replaced the
+ * original. Generally you will not want to do that. Note this method
+ * animates the node's transform, but does not directly change the node's
+ * bounds rectangle. Use animateToBounds() to animate the node's bounds
+ * rectangle instead.
+ *
+ * @param x left of target bounds
+ * @param y top of target bounds
+ * @param width width of target bounds
+ * @param height height of target bounds
+ * @param duration amount of time that the animation should take
+ * @return the newly scheduled activity
+ */
+ public PTransformActivity animateTransformToBounds(final double x, final double y, final double width,
+ final double height, final long duration) {
+ final PAffineTransform t = new PAffineTransform();
+ t.setToScale(width / getWidth(), height / getHeight());
+ final double scale = t.getScale();
+ t.setOffset(x - getX() * scale, y - getY() * scale);
+ return animateToTransform(t, duration);
+ }
+
+ /**
+ * Animate this node's transform from its current location when the activity
+ * starts to the specified location, scale, and rotation. If this node
+ * descends from the root then the activity will be scheduled, else the
+ * returned activity should be scheduled manually. If two different
+ * transform activities are scheduled for the same node at the same time,
+ * they will both be applied to the node, but the last one scheduled will be
+ * applied last on each frame, so it will appear to have replaced the
+ * original. Generally you will not want to do that.
+ *
+ * @param x the final target x position of node
+ * @param y the final target y position of node
+ * @param duration amount of time that the animation should take
+ * @param scale the final scale for the duration
+ * @param theta final theta value (in radians) for the animation
+ * @return the newly scheduled activity
+ */
+ public PTransformActivity animateToPositionScaleRotation(final double x, final double y, final double scale,
+ final double theta, final long duration) {
+ final PAffineTransform t = getTransform();
+ t.setOffset(x, y);
+ t.setScale(scale);
+ t.setRotation(theta);
+ return animateToTransform(t, duration);
+ }
+
+ /**
+ * Animate this node's transform from its current values when the activity
+ * starts to the new values specified in the given transform. If this node
+ * descends from the root then the activity will be scheduled, else the
+ * returned activity should be scheduled manually. If two different
+ * transform activities are scheduled for the same node at the same time,
+ * they will both be applied to the node, but the last one scheduled will be
+ * applied last on each frame, so it will appear to have replaced the
+ * original. Generally you will not want to do that.
+ *
+ * @param destTransform the final transform value
+ * @param duration amount of time that the animation should take
+ * @return the newly scheduled activity
+ */
+ public PTransformActivity animateToTransform(final AffineTransform destTransform, final long duration) {
+ if (duration == 0) {
+ setTransform(destTransform);
+ return null;
+ }
+ else {
+ final PTransformActivity.Target t = new PTransformActivity.Target() {
+ public void setTransform(final AffineTransform aTransform) {
+ PNode.this.setTransform(aTransform);
+ }
+
+ public void getSourceMatrix(final double[] aSource) {
+ PNode.this.getTransformReference(true).getMatrix(aSource);
+ }
+ };
+
+ final PTransformActivity ta = new PTransformActivity(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, t,
+ destTransform);
+ addActivity(ta);
+ return ta;
+ }
+ }
+
+ /**
+ * Animate this node's color from its current value to the new value
+ * specified. This meathod assumes that this nodes paint property is of type
+ * color. If this node descends from the root then the activity will be
+ * scheduled, else the returned activity should be scheduled manually. If
+ * two different color activities are scheduled for the same node at the
+ * same time, they will both be applied to the node, but the last one
+ * scheduled will be applied last on each frame, so it will appear to have
+ * replaced the original. Generally you will not want to do that.
+ *
+ * @param destColor final color value.
+ * @param duration amount of time that the animation should take
+ * @return the newly scheduled activity
+ */
+ public PInterpolatingActivity animateToColor(final Color destColor, final long duration) {
+ if (duration == 0) {
+ setPaint(destColor);
+ return null;
+ }
+ else {
+ final PColorActivity.Target t = new PColorActivity.Target() {
+ public Color getColor() {
+ return (Color) getPaint();
+ }
+
+ public void setColor(final Color color) {
+ setPaint(color);
+ }
+ };
+
+ final PColorActivity ca = new PColorActivity(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, t, destColor);
+ addActivity(ca);
+ return ca;
+ }
+ }
+
+
+ /**
+ * Animate this node's transparency from its current value to the new value
+ * specified. Transparency values must range from zero to one. If this node
+ * descends from the root then the activity will be scheduled, else the
+ * returned activity should be scheduled manually. If two different
+ * transparency activities are scheduled for the same node at the same time,
+ * they will both be applied to the node, but the last one scheduled will be
+ * applied last on each frame, so it will appear to have replaced the
+ * original. Generally you will not want to do that.
+ *
+ * @param zeroToOne final transparency value.
+ * @param duration amount of time that the animation should take
+ * @return the newly scheduled activity
+ */
+ public PInterpolatingActivity animateToTransparency(final float zeroToOne, final long duration) {
+ if (duration == 0) {
+ setTransparency(zeroToOne);
+ return null;
+ }
+ else {
+ final float dest = zeroToOne;
+
+ final PInterpolatingActivity ta = new PInterpolatingActivity(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE) {
+ private float source;
+
+ protected void activityStarted() {
+ source = getTransparency();
+ super.activityStarted();
+ }
+
+ public void setRelativeTargetValue(final float zeroToOne) {
+ PNode.this.setTransparency(source + zeroToOne * (dest - source));
+ }
+ };
+
+ addActivity(ta);
+ return ta;
+ }
+ }
+
+ /**
+ * Schedule the given activity with the root, note that only scheduled
+ * activities will be stepped. If the activity is successfully added true is
+ * returned, else false.
+ *
+ * @param activity new activity to schedule
+ * @return true if the activity is successfully scheduled.
+ */
+ public boolean addActivity(final PActivity activity) {
+ final PRoot r = getRoot();
+ if (r != null) {
+ return r.addActivity(activity);
+ }
+ return false;
+ }
+
+ // ****************************************************************
+ // Client Properties - Methods for managing client properties for
+ // this node.
+ //
+ // Client properties provide a way for programmers to attach
+ // extra information to a node without having to subclass it and
+ // add new instance variables.
+ // ****************************************************************
+
+ /**
+ * Return mutable attributed set of client properties associated with this
+ * node.
+ *
+ * @return the client properties associated to this node
+ */
+ public MutableAttributeSet getClientProperties() {
+ if (clientProperties == null) {
+ clientProperties = new SimpleAttributeSet();
+ }
+ return clientProperties;
+ }
+
+ /**
+ * Returns the value of the client attribute with the specified key. Only
+ * attributes added with addAttribute
will return a non-null
+ * value.
+ *
+ * @param key key to use while fetching client attribute
+ *
+ * @return the value of this attribute or null
+ */
+ public Object getAttribute(final Object key) {
+ if (clientProperties == null || key == null) {
+ return null;
+ }
+ else {
+ return clientProperties.getAttribute(key);
+ }
+ }
+
+ /**
+ * Add an arbitrary key/value to this node.
+ *
+ * The get/add attribute
methods provide access to a small
+ * per-instance attribute set. Callers can use get/add attribute to annotate
+ * nodes that were created by another module.
+ *
+ * If value is null this method will remove the attribute.
+ *
+ * @param key to use when adding the attribute
+ * @param value value to associate to the new attribute
+ */
+ public void addAttribute(final Object key, final Object value) {
+ if (value == null && clientProperties == null) {
+ return;
+ }
+
+ final Object oldValue = getAttribute(key);
+
+ if (value != oldValue) {
+ if (clientProperties == null) {
+ clientProperties = new SimpleAttributeSet();
+ }
+
+ if (value == null) {
+ clientProperties.removeAttribute(key);
+ }
+ else {
+ clientProperties.addAttribute(key, value);
+ }
+
+ if (clientProperties.getAttributeCount() == 0 && clientProperties.getResolveParent() == null) {
+ clientProperties = null;
+ }
+
+ firePropertyChange(PROPERTY_CODE_CLIENT_PROPERTIES, PROPERTY_CLIENT_PROPERTIES, null, clientProperties);
+ firePropertyChange(PROPERTY_CODE_CLIENT_PROPERTIES, key.toString(), oldValue, value);
+ }
+ }
+
+ /**
+ * Returns an enumeration of all keys maped to attribute values values.
+ *
+ * @return an Enumeration over attribute keys
+ */
+ public Enumeration getClientPropertyKeysEnumeration() {
+ if (clientProperties == null) {
+ return PUtil.NULL_ENUMERATION;
+ }
+ else {
+ return clientProperties.getAttributeNames();
+ }
+ }
+
+ // convenience methods for attributes
+
+ /**
+ * Fetches the value of the requested attribute, returning defaultValue is
+ * not found.
+ *
+ * @param key attribute to search for
+ * @param defaultValue value to return if attribute is not found
+ *
+ * @return value of attribute or defaultValue if not found
+ */
+ public Object getAttribute(final Object key, final Object defaultValue) {
+ final Object value = getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ return value;
+ }
+
+ /**
+ * Fetches the boolean value of the requested attribute, returning
+ * defaultValue is not found.
+ *
+ * @param key attribute to search for
+ * @param defaultValue value to return if attribute is not found
+ *
+ * @return value of attribute or defaultValue if not found
+ */
+ public boolean getBooleanAttribute(final Object key, final boolean defaultValue) {
+ final Boolean value = (Boolean) getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ return value.booleanValue();
+ }
+
+ /**
+ * Fetches the integer value of the requested attribute, returning
+ * defaultValue is not found.
+ *
+ * @param key attribute to search for
+ * @param defaultValue value to return if attribute is not found
+ *
+ * @return value of attribute or defaultValue if not found
+ */
+ public int getIntegerAttribute(final Object key, final int defaultValue) {
+ final Number value = (Number) getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ return value.intValue();
+ }
+
+ /**
+ * Fetches the double value of the requested attribute, returning
+ * defaultValue is not found.
+ *
+ * @param key attribute to search for
+ * @param defaultValue value to return if attribute is not found
+ *
+ * @return value of attribute or defaultValue if not found
+ */
+ public double getDoubleAttribute(final Object key, final double defaultValue) {
+ final Number value = (Number) getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ return value.doubleValue();
+ }
+
+ /**
+ * @deprecated use getAttribute(Object key)instead.
+ *
+ * @param key name of property to search for
+ * @return value of matching client property
+ */
+ public Object getClientProperty(final Object key) {
+ return getAttribute(key);
+ }
+
+ /**
+ * @deprecated use addAttribute(Object key, Object value)instead.
+ *
+ * @param key name of property to add
+ * @param value value or new attribute
+ */
+ public void addClientProperty(final Object key, final Object value) {
+ addAttribute(key, value);
+ }
+
+ /**
+ * @deprecated use getClientPropertyKeysEnumerator() instead.
+ *
+ * @return iterator for client property keys
+ */
+ public Iterator getClientPropertyKeysIterator() {
+ final Enumeration enumeration = getClientPropertyKeysEnumeration();
+
+ return new ClientPropertyKeyIterator(enumeration);
+ }
+
+ // ****************************************************************
+ // Copying - Methods for copying this node and its descendants.
+ // Copying is implemented in terms of serialization.
+ // ****************************************************************
+
+ /**
+ * The copy method copies this node and all of its descendants. Note that
+ * copying is implemented in terms of java serialization. See the
+ * serialization notes for more information.
+ *
+ * @return new copy of this node or null if the node was not serializable
+ */
+ public Object clone() {
+ try {
+ final byte[] ser = PObjectOutputStream.toByteArray(this);
+ return new ObjectInputStream(new ByteArrayInputStream(ser)).readObject();
+ }
+ catch (final IOException e) {
+ return null;
+ }
+ catch (final ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ // ****************************************************************
+ // Coordinate System Conversions - Methods for converting
+ // geometry between this nodes local coordinates and the other
+ // major coordinate systems.
+ //
+ // Each nodes has an affine transform that it uses to define its
+ // own coordinate system. For example if you create a new node and
+ // add it to the canvas it will appear in the upper right corner. Its
+ // coordinate system matches the coordinate system of its parent
+ // (the root node) at this point. But if you move this node by calling
+ // node.translate() the nodes affine transform will be modified and the
+ // node will appear at a different location on the screen. The node
+ // coordinate system no longer matches the coordinate system of its
+ // parent.
+ //
+ // This is useful because it means that the node's methods for
+ // rendering and picking don't need to worry about the fact that
+ // the node has been moved to another position on the screen, they
+ // keep working just like they did when it was in the upper right
+ // hand corner of the screen.
+ //
+ // The problem is now that each node defines its own coordinate
+ // system it is difficult to compare the positions of two node with
+ // each other. These methods are all meant to help solve that problem.
+ //
+ // The terms used in the methods are as follows:
+ //
+ // local - The local or base coordinate system of a node.
+ // parent - The coordinate system of a node's parent
+ // global - The topmost coordinate system, above the root node.
+ //
+ // Normally when comparing the positions of two nodes you will
+ // convert the local position of each node to the global coordinate
+ // system, and then compare the positions in that common coordinate
+ // system.
+ // ***************************************************************
+
+ /**
+ * Transform the given point from this node's local coordinate system to its
+ * parent's local coordinate system. Note that this will modify the point
+ * parameter.
+ *
+ * @param localPoint point in local coordinate system to be transformed.
+ * @return point in parent's local coordinate system
+ */
+ public Point2D localToParent(final Point2D localPoint) {
+ if (transform == null) {
+ return localPoint;
+ }
+ return transform.transform(localPoint, localPoint);
+ }
+
+ /**
+ * Transform the given dimension from this node's local coordinate system to
+ * its parent's local coordinate system. Note that this will modify the
+ * dimension parameter.
+ *
+ * @param localDimension dimension in local coordinate system to be
+ * transformed.
+ * @return dimension in parent's local coordinate system
+ */
+ public Dimension2D localToParent(final Dimension2D localDimension) {
+ if (transform == null) {
+ return localDimension;
+ }
+ return transform.transform(localDimension, localDimension);
+ }
+
+ /**
+ * Transform the given rectangle from this node's local coordinate system to
+ * its parent's local coordinate system. Note that this will modify the
+ * rectangle parameter.
+ *
+ * @param localRectangle rectangle in local coordinate system to be
+ * transformed.
+ * @return rectangle in parent's local coordinate system
+ */
+ public Rectangle2D localToParent(final Rectangle2D localRectangle) {
+ if (transform == null) {
+ return localRectangle;
+ }
+ return transform.transform(localRectangle, localRectangle);
+ }
+
+ /**
+ * Transform the given point from this node's parent's local coordinate
+ * system to the local coordinate system of this node. Note that this will
+ * modify the point parameter.
+ *
+ * @param parentPoint point in parent's coordinate system to be transformed.
+ * @return point in this node's local coordinate system
+ */
+ public Point2D parentToLocal(final Point2D parentPoint) {
+ if (transform == null) {
+ return parentPoint;
+ }
+
+ return transform.inverseTransform(parentPoint, parentPoint);
+ }
+
+ /**
+ * Transform the given dimension from this node's parent's local coordinate
+ * system to the local coordinate system of this node. Note that this will
+ * modify the dimension parameter.
+ *
+ * @param parentDimension dimension in parent's coordinate system to be
+ * transformed.
+ * @return dimension in this node's local coordinate system
+ */
+ public Dimension2D parentToLocal(final Dimension2D parentDimension) {
+ if (transform == null) {
+ return parentDimension;
+ }
+ return transform.inverseTransform(parentDimension, parentDimension);
+ }
+
+ /**
+ * Transform the given rectangle from this node's parent's local coordinate
+ * system to the local coordinate system of this node. Note that this will
+ * modify the rectangle parameter.
+ *
+ * @param parentRectangle rectangle in parent's coordinate system to be
+ * transformed.
+ * @return rectangle in this node's local coordinate system
+ */
+ public Rectangle2D parentToLocal(final Rectangle2D parentRectangle) {
+ if (transform == null) {
+ return parentRectangle;
+ }
+ return transform.inverseTransform(parentRectangle, parentRectangle);
+ }
+
+ /**
+ * Transform the given point from this node's local coordinate system to the
+ * global coordinate system. Note that this will modify the point parameter.
+ *
+ * @param localPoint point in local coordinate system to be transformed.
+ * @return point in global coordinates
+ */
+ public Point2D localToGlobal(final Point2D localPoint) {
+ PNode n = this;
+ while (n != null) {
+ n.localToParent(localPoint);
+ n = n.parent;
+ }
+ return localPoint;
+ }
+
+ /**
+ * Transform the given dimension from this node's local coordinate system to
+ * the global coordinate system. Note that this will modify the dimension
+ * parameter.
+ *
+ * @param localDimension dimension in local coordinate system to be
+ * transformed.
+ * @return dimension in global coordinates
+ */
+ public Dimension2D localToGlobal(final Dimension2D localDimension) {
+ PNode n = this;
+ while (n != null) {
+ n.localToParent(localDimension);
+ n = n.parent;
+ }
+ return localDimension;
+ }
+
+ /**
+ * Transform the given rectangle from this node's local coordinate system to
+ * the global coordinate system. Note that this will modify the rectangle
+ * parameter.
+ *
+ * @param localRectangle rectangle in local coordinate system to be
+ * transformed.
+ * @return rectangle in global coordinates
+ */
+ public Rectangle2D localToGlobal(final Rectangle2D localRectangle) {
+ PNode n = this;
+ while (n != null) {
+ n.localToParent(localRectangle);
+ n = n.parent;
+ }
+ return localRectangle;
+ }
+
+ /**
+ * Transform the given point from global coordinates to this node's local
+ * coordinate system. Note that this will modify the point parameter.
+ *
+ * @param globalPoint point in global coordinates to be transformed.
+ * @return point in this node's local coordinate system.
+ */
+ public Point2D globalToLocal(final Point2D globalPoint) {
+ final PAffineTransform globalTransform = computeGlobalTransform(this);
+ return globalTransform.inverseTransform(globalPoint, globalPoint);
+ }
+
+ private PAffineTransform computeGlobalTransform(final PNode node) {
+ if (node == null) {
+ return new PAffineTransform();
+ }
+
+ final PAffineTransform parentGlobalTransform = computeGlobalTransform(node.parent);
+ if (node.transform != null) {
+ parentGlobalTransform.concatenate(node.transform);
+ }
+ return parentGlobalTransform;
+ }
+
+ /**
+ * Transform the given dimension from global coordinates to this node's
+ * local coordinate system. Note that this will modify the dimension
+ * parameter.
+ *
+ * @param globalDimension dimension in global coordinates to be transformed.
+ * @return dimension in this node's local coordinate system.
+ */
+ public Dimension2D globalToLocal(final Dimension2D globalDimension) {
+ if (parent != null) {
+ parent.globalToLocal(globalDimension);
+ }
+ return parentToLocal(globalDimension);
+ }
+
+ /**
+ * Transform the given rectangle from global coordinates to this node's
+ * local coordinate system. Note that this will modify the rectangle
+ * parameter.
+ *
+ * @param globalRectangle rectangle in global coordinates to be transformed.
+ * @return rectangle in this node's local coordinate system.
+ */
+ public Rectangle2D globalToLocal(final Rectangle2D globalRectangle) {
+ if (parent != null) {
+ parent.globalToLocal(globalRectangle);
+ }
+ return parentToLocal(globalRectangle);
+ }
+
+ /**
+ * Return the transform that converts local coordinates at this node to the
+ * global coordinate system.
+ *
+ * @param dest PAffineTransform to transform to global coordinates
+ * @return The concatenation of transforms from the top node down to this
+ * node.
+ */
+ public PAffineTransform getLocalToGlobalTransform(final PAffineTransform dest) {
+ PAffineTransform result = dest;
+ if (parent != null) {
+ result = parent.getLocalToGlobalTransform(result);
+ if (transform != null) {
+ result.concatenate(transform);
+ }
+ }
+ else if (dest == null) {
+ result = getTransform();
+ }
+ else if (transform != null) {
+ result.setTransform(transform);
+ }
+ else {
+ result.setToIdentity();
+ }
+
+ return result;
+ }
+
+ /**
+ * Return the transform that converts global coordinates to local
+ * coordinates of this node.
+ *
+ * @param dest PAffineTransform to transform from global to local
+ *
+ * @return The inverse of the concatenation of transforms from the root down
+ * to this node.
+ */
+ public PAffineTransform getGlobalToLocalTransform(final PAffineTransform dest) {
+ PAffineTransform result = getLocalToGlobalTransform(dest);
+ try {
+ result.setTransform(result.createInverse());
+ }
+ catch (final NoninvertibleTransformException e) {
+ throw new PAffineTransformException(e, result);
+ }
+ return result;
+ }
+
+ // ****************************************************************
+ // Event Listeners - Methods for adding and removing event listeners
+ // from a node.
+ //
+ // Here methods are provided to add property change listeners and
+ // input event listeners. The property change listeners are notified
+ // when certain properties of this node change, and the input event
+ // listeners are notified when the nodes receives new key and mouse
+ // events.
+ // ****************************************************************
+
+ /**
+ * Return the list of event listeners associated with this node.
+ *
+ * @return event listener list or null
+ */
+ public EventListenerList getListenerList() {
+ return listenerList;
+ }
+
+ /**
+ * Adds the specified input event listener to receive input events from this
+ * node.
+ *
+ * @param listener the new input listener
+ */
+ public void addInputEventListener(final PInputEventListener listener) {
+ if (listenerList == null) {
+ listenerList = new EventListenerList();
+ }
+ getListenerList().add(PInputEventListener.class, listener);
+ }
+
+ /**
+ * Removes the specified input event listener so that it no longer receives
+ * input events from this node.
+ *
+ * @param listener the input listener to remove
+ */
+ public void removeInputEventListener(final PInputEventListener listener) {
+ if (listenerList == null) {
+ return;
+ }
+ getListenerList().remove(PInputEventListener.class, listener);
+ if (listenerList.getListenerCount() == 0) {
+ listenerList = null;
+ }
+ }
+
+ /**
+ * Add a PropertyChangeListener to the listener list. The listener is
+ * registered for all properties. See the fields in PNode and subclasses
+ * that start with PROPERTY_ to find out which properties exist.
+ *
+ * @param listener The PropertyChangeListener to be added
+ */
+ public void addPropertyChangeListener(final PropertyChangeListener listener) {
+ if (changeSupport == null) {
+ changeSupport = new SwingPropertyChangeSupport(this);
+ }
+ changeSupport.addPropertyChangeListener(listener);
+ }
+
+ /**
+ * Add a PropertyChangeListener for a specific property. The listener will
+ * be invoked only when a call on firePropertyChange names that specific
+ * property. See the fields in PNode and subclasses that start with
+ * PROPERTY_ to find out which properties are supported.
+ *
+ * @param propertyName The name of the property to listen on.
+ * @param listener The PropertyChangeListener to be added
+ */
+ public void addPropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
+ if (listener == null) {
+ return;
+ }
+ if (changeSupport == null) {
+ changeSupport = new SwingPropertyChangeSupport(this);
+ }
+ changeSupport.addPropertyChangeListener(propertyName, listener);
+ }
+
+ /**
+ * Remove a PropertyChangeListener from the listener list. This removes a
+ * PropertyChangeListener that was registered for all properties.
+ *
+ * @param listener The PropertyChangeListener to be removed
+ */
+ public void removePropertyChangeListener(final PropertyChangeListener listener) {
+ if (changeSupport != null) {
+ changeSupport.removePropertyChangeListener(listener);
+ }
+ }
+
+ /**
+ * Remove a PropertyChangeListener for a specific property.
+ *
+ * @param propertyName The name of the property that was listened on.
+ * @param listener The PropertyChangeListener to be removed
+ */
+ public void removePropertyChangeListener(final String propertyName, final PropertyChangeListener listener) {
+ if (listener == null) {
+ return;
+ }
+ if (changeSupport == null) {
+ return;
+ }
+ changeSupport.removePropertyChangeListener(propertyName, listener);
+ }
+
+ /**
+ * Return the propertyChangeParentMask that determines which property change
+ * events are forwared to this nodes parent so that its property change
+ * listeners will also be notified.
+ *
+ * @return mask used for deciding whether to bubble property changes up to
+ * parent
+ */
+ public int getPropertyChangeParentMask() {
+ return propertyChangeParentMask;
+ }
+
+ /**
+ * Set the propertyChangeParentMask that determines which property change
+ * events are forwared to this nodes parent so that its property change
+ * listeners will also be notified.
+ *
+ * @param propertyChangeParentMask new mask for property change bubble up
+ */
+ public void setPropertyChangeParentMask(final int propertyChangeParentMask) {
+ this.propertyChangeParentMask = propertyChangeParentMask;
+ }
+
+ /**
+ * Report a bound property update to any registered listeners. No event is
+ * fired if old and new are equal and non-null. If the propertyCode exists
+ * in this node's propertyChangeParentMask then a property change event will
+ * also be fired on this nodes parent.
+ *
+ * @param propertyCode The code of the property changed.
+ * @param propertyName The name of the property that was changed.
+ * @param oldValue The old value of the property.
+ * @param newValue The new value of the property.
+ */
+ protected void firePropertyChange(final int propertyCode, final String propertyName, final Object oldValue,
+ final Object newValue) {
+ PropertyChangeEvent event = null;
+
+ if (changeSupport != null) {
+ event = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
+ changeSupport.firePropertyChange(event);
+ }
+ if (parent != null && (propertyCode & propertyChangeParentMask) != 0) {
+ if (event == null) {
+ event = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
+ }
+ parent.fireChildPropertyChange(event, propertyCode);
+ }
+ }
+
+ /**
+ * Called by child node to forward property change events up the node tree
+ * so that property change listeners registered with this node will be
+ * notified of property changes of its children nodes. For performance
+ * reason only propertyCodes listed in the propertyChangeParentMask are
+ * forwarded.
+ *
+ * @param event The property change event containing source node and changed
+ * values.
+ * @param propertyCode The code of the property changed.
+ */
+ protected void fireChildPropertyChange(final PropertyChangeEvent event, final int propertyCode) {
+ if (changeSupport != null) {
+ changeSupport.firePropertyChange(event);
+ }
+ if (parent != null && (propertyCode & propertyChangeParentMask) != 0) {
+ parent.fireChildPropertyChange(event, propertyCode);
+ }
+ }
+
+ // ****************************************************************
+ // Bounds Geometry - Methods for setting and querying the bounds
+ // of this node.
+ //
+ // The bounds of a node store the node's position and size in
+ // the nodes local coordinate system. Many node subclasses will need
+ // to override the setBounds method so that they can update their
+ // internal state appropriately. See PPath for an example.
+ //
+ // Since the bounds are stored in the local coordinate system
+ // they WILL NOT change if the node is scaled, translated, or rotated.
+ //
+ // The bounds may be accessed with either getBounds, or
+ // getBoundsReference. The former returns a copy of the bounds
+ // the latter returns a reference to the nodes bounds that should
+ // normally not be modified. If a node is marked as volatile then
+ // it may modify its bounds before returning them from getBoundsReference,
+ // otherwise it may not.
+ // ****************************************************************
+
+ /**
+ * Return a copy of this node's bounds. These bounds are stored in the local
+ * coordinate system of this node and do not include the bounds of any of
+ * this node's children.
+ *
+ * @return copy of this node's local bounds
+ */
+ public PBounds getBounds() {
+ return (PBounds) getBoundsReference().clone();
+ }
+
+ /**
+ * Return a direct reference to this node's bounds. These bounds are stored
+ * in the local coordinate system of this node and do not include the bounds
+ * of any of this node's children. The value returned should not be
+ * modified.
+ *
+ * @return direct reference to local bounds
+ */
+ public PBounds getBoundsReference() {
+ return bounds;
+ }
+
+ /**
+ * Notify this node that you will begin to repeatedly call setBounds
+ *
. When you
+ * are done call endResizeBounds
to let the node know that you
+ * are done.
+ */
+ public void startResizeBounds() {
+ }
+
+ /**
+ * Notify this node that you have finished a resize bounds sequence.
+ */
+ public void endResizeBounds() {
+ }
+
+ /**
+ * Set's this node's bounds left position, leaving y, width, and height
+ * unchanged.
+ *
+ * @param x new x position of bounds
+ *
+ * @return whether the change was successful
+ */
+ public boolean setX(final double x) {
+ return setBounds(x, getY(), getWidth(), getHeight());
+ }
+
+ /**
+ * Set's this node's bounds top position, leaving x, width, and height
+ * unchanged.
+ *
+ * @param y new y position of bounds
+ *
+ * @return whether the change was successful
+ */
+ public boolean setY(final double y) {
+ return setBounds(getX(), y, getWidth(), getHeight());
+ }
+
+ /**
+ * Set's this node's bounds width, leaving x, y, and height unchanged.
+ *
+ * @param width new width position of bounds
+ *
+ * @return whether the change was successful
+ */
+ public boolean setWidth(final double width) {
+ return setBounds(getX(), getY(), width, getHeight());
+ }
+
+ /**
+ * Set's this node's bounds height, leaving x, y, and width unchanged.
+ *
+ * @param height new height position of bounds
+ *
+ * @return whether the change was successful
+ */
+ public boolean setHeight(final double height) {
+ return setBounds(getX(), getY(), getWidth(), height);
+ }
+
+ /**
+ * Set the bounds of this node to the given value. These bounds are stored
+ * in the local coordinate system of this node.
+ *
+ * @param newBounds bounds to apply to this node
+ *
+ * @return true if the bounds changed.
+ */
+ public boolean setBounds(final Rectangle2D newBounds) {
+ return setBounds(newBounds.getX(), newBounds.getY(), newBounds.getWidth(), newBounds.getHeight());
+ }
+
+ /**
+ * Set the bounds of this node to the given position and size. These bounds
+ * are stored in the local coordinate system of this node.
+ *
+ * If the width or height is less then or equal to zero then the bound's
+ * empty bit will be set to true.
+ *
+ * Subclasses must call the super.setBounds() method.
+ *
+ * @param x x position of bounds
+ * @param y y position of bounds
+ * @param width width to apply to the bounds
+ * @param height height to apply to the bounds
+ *
+ * @return true if the bounds changed.
+ */
+ public boolean setBounds(final double x, final double y, final double width, final double height) {
+ if (bounds.x != x || bounds.y != y || bounds.width != width || bounds.height != height) {
+ bounds.setRect(x, y, width, height);
+
+ if (width <= 0 || height <= 0) {
+ bounds.reset();
+ }
+
+ internalUpdateBounds(x, y, width, height);
+ invalidatePaint();
+ signalBoundsChanged();
+ return true;
+ }
+ // Don't put any invalidating code here or else nodes with volatile
+ // bounds will
+ // create a soft infinite loop (calling Swing.invokeLater()) when they
+ // validate
+ // their bounds.
+ return false;
+ }
+
+ /**
+ * Gives nodes a chance to update their internal structure before bounds
+ * changed notifications are sent. When this message is recived the nodes
+ * bounds field will contain the new value.
+ *
+ * See PPath for an example that uses this method.
+ *
+ * @param x x position of bounds
+ * @param y y position of bounds
+ * @param width width to apply to the bounds
+ * @param height height to apply to the bounds
+ */
+ protected void internalUpdateBounds(final double x, final double y, final double width, final double height) {
+ }
+
+ /**
+ * Set the empty bit of this bounds to true.
+ */
+ public void resetBounds() {
+ setBounds(0, 0, 0, 0);
+ }
+
+ /**
+ * Return the x position (in local coords) of this node's bounds.
+ *
+ * @return local x position of bounds
+ */
+ public double getX() {
+ return getBoundsReference().getX();
+ }
+
+ /**
+ * Return the y position (in local coords) of this node's bounds.
+ *
+ * @return local y position of bounds
+ */
+ public double getY() {
+ return getBoundsReference().getY();
+ }
+
+ /**
+ * Return the width (in local coords) of this node's bounds.
+ *
+ * @return local width of bounds
+ */
+ public double getWidth() {
+ return getBoundsReference().getWidth();
+ }
+
+ /**
+ * Return the height (in local coords) of this node's bounds.
+ *
+ * @return local width of bounds
+ */
+ public double getHeight() {
+ return getBoundsReference().getHeight();
+ }
+
+ /**
+ * Return a copy of the bounds of this node in the global coordinate system.
+ *
+ * @return the bounds in global coordinate system.
+ */
+ public PBounds getGlobalBounds() {
+ return (PBounds) localToGlobal(getBounds());
+ }
+
+ /**
+ * Center the bounds of this node so that they are centered on the given
+ * point specified on the local coordinates of this node. Note that this
+ * method will modify the nodes bounds, while centerFullBoundsOnPoint will
+ * modify the nodes transform.
+ *
+ * @param localX x position of point around which to center bounds
+ * @param localY y position of point around which to center bounds
+ *
+ * @return true if the bounds changed.
+ */
+ public boolean centerBoundsOnPoint(final double localX, final double localY) {
+ final double dx = localX - bounds.getCenterX();
+ final double dy = localY - bounds.getCenterY();
+ return setBounds(bounds.x + dx, bounds.y + dy, bounds.width, bounds.height);
+ }
+
+ /**
+ * Center the full bounds of this node so that they are centered on the
+ * given point specified on the local coordinates of this nodes parent. Note
+ * that this method will modify the nodes transform, while
+ * centerBoundsOnPoint will modify the nodes bounds.
+ *
+ * @param parentX x position around which to center full bounds
+ * @param parentY y position around which to center full bounds
+ */
+ public void centerFullBoundsOnPoint(final double parentX, final double parentY) {
+ final double dx = parentX - getFullBoundsReference().getCenterX();
+ final double dy = parentY - getFullBoundsReference().getCenterY();
+ offset(dx, dy);
+ }
+
+ /**
+ * Return true if this node intersects the given rectangle specified in
+ * local bounds. If the geometry of this node is complex this method can
+ * become expensive, it is therefore recommended that
+ * fullIntersects
is used for quick rejects before calling this
+ * method.
+ *
+ * @param localBounds the bounds to test for intersection against
+ * @return true if the given rectangle intersects this nodes geometry.
+ */
+ public boolean intersects(final Rectangle2D localBounds) {
+ if (localBounds == null) {
+ return true;
+ }
+ return getBoundsReference().intersects(localBounds);
+ }
+
+ // ****************************************************************
+ // Full Bounds - Methods for computing and querying the
+ // full bounds of this node.
+ //
+ // The full bounds of a node store the nodes bounds
+ // together with the union of the bounds of all the
+ // node's descendants. The full bounds are stored in the parent
+ // coordinate system of this node, the full bounds DOES change
+ // when you translate, scale, or rotate this node.
+ //
+ // The full bounds may be accessed with either getFullBounds, or
+ // getFullBoundsReference. The former returns a copy of the full bounds
+ // the latter returns a reference to the node's full bounds that should
+ // not be modified.
+ // ****************************************************************
+
+ /**
+ * Return a copy of this node's full bounds. These bounds are stored in the
+ * parent coordinate system of this node and they include the union of this
+ * node's bounds and all the bounds of it's descendants.
+ *
+ * @return a copy of this node's full bounds.
+ */
+ public PBounds getFullBounds() {
+ return (PBounds) getFullBoundsReference().clone();
+ }
+
+ /**
+ * Return a reference to this node's full bounds cache. These bounds are
+ * stored in the parent coordinate system of this node and they include the
+ * union of this node's bounds and all the bounds of it's descendants. The
+ * bounds returned by this method should not be modified.
+ *
+ * @return a reference to this node's full bounds cache.
+ */
+ public PBounds getFullBoundsReference() {
+ validateFullBounds();
+ return fullBoundsCache;
+ }
+
+ /**
+ * Compute and return the full bounds of this node. If the dstBounds
+ * parameter is not null then it will be used to return the results instead
+ * of creating a new PBounds.
+ *
+ * @param dstBounds if not null the new bounds will be stored here
+ * @return the full bounds in the parent coordinate system of this node
+ */
+ public PBounds computeFullBounds(final PBounds dstBounds) {
+ final PBounds result = getUnionOfChildrenBounds(dstBounds);
+ result.add(getBoundsReference());
+ localToParent(result);
+ return result;
+ }
+
+ /**
+ * Compute and return the union of the full bounds of all the children of
+ * this node. If the dstBounds parameter is not null then it will be used to
+ * return the results instead of creating a new PBounds.
+ *
+ * @param dstBounds if not null the new bounds will be stored here
+ * @return union of children bounds
+ */
+ public PBounds getUnionOfChildrenBounds(final PBounds dstBounds) {
+ PBounds resultBounds;
+ if (dstBounds == null) {
+ resultBounds = new PBounds();
+ }
+ else {
+ resultBounds = dstBounds;
+ resultBounds.resetToZero();
+ }
+
+ final int count = getChildrenCount();
+ for (int i = 0; i < count; i++) {
+ if (children.size()<=i) break; /*miuramo*/
+ final PNode each = (PNode) children.get(i);
+ resultBounds.add(each.getFullBoundsReference());
+ }
+
+ return resultBounds;
+ }
+
+ /**
+ * Return a copy of the full bounds of this node in the global coordinate
+ * system.
+ *
+ * @return the full bounds in global coordinate system.
+ */
+ public PBounds getGlobalFullBounds() {
+ final PBounds b = getFullBounds();
+ if (parent != null) {
+ parent.localToGlobal(b);
+ }
+ return b;
+ }
+
+ /**
+ * Return true if the full bounds of this node intersects with the specified
+ * bounds.
+ *
+ * @param parentBounds the bounds to test for intersection against
+ * (specified in parent's coordinate system)
+ * @return true if this nodes full bounds intersect the given bounds.
+ */
+ public boolean fullIntersects(final Rectangle2D parentBounds) {
+ if (parentBounds == null) {
+ return true;
+ }
+ return getFullBoundsReference().intersects(parentBounds);
+ }
+
+ // ****************************************************************
+ // Bounds Damage Management - Methods used to invalidate and validate
+ // the bounds of nodes.
+ // ****************************************************************
+
+ /**
+ * Return true if this nodes bounds may change at any time. The default
+ * behavior is to return false, subclasses that override this method to
+ * return true should also override getBoundsReference() and compute their
+ * volatile bounds there before returning the reference.
+ *
+ * @return true if this node has volatile bounds
+ */
+ protected boolean getBoundsVolatile() {
+ return false;
+ }
+
+ /**
+ * Return true if this node has a child with volatile bounds.
+ *
+ * @return true if this node has a child with volatile bounds
+ */
+ protected boolean getChildBoundsVolatile() {
+ return childBoundsVolatile;
+ }
+
+ /**
+ * Set if this node has a child with volatile bounds. This should normally
+ * be managed automatically by the bounds validation process.
+ *
+ * @param childBoundsVolatile true if this node has a descendant with
+ * volatile bounds
+ */
+ protected void setChildBoundsVolatile(final boolean childBoundsVolatile) {
+ this.childBoundsVolatile = childBoundsVolatile;
+ }
+
+ /**
+ * Return true if this node's bounds have recently changed. This flag will
+ * be reset on the next call of validateFullBounds.
+ *
+ * @return true if this node's bounds have changed.
+ */
+ protected boolean getBoundsChanged() {
+ return boundsChanged;
+ }
+
+ /**
+ * Set the bounds chnaged flag. This flag will be reset on the next call of
+ * validateFullBounds.
+ *
+ * @param boundsChanged true if this nodes bounds have changed.
+ */
+ protected void setBoundsChanged(final boolean boundsChanged) {
+ this.boundsChanged = boundsChanged;
+ }
+
+ /**
+ * Return true if the full bounds of this node are invalid. This means that
+ * the full bounds of this node have changed and need to be recomputed.
+ *
+ * @return true if the full bounds of this node are invalid
+ */
+ protected boolean getFullBoundsInvalid() {
+ return fullBoundsInvalid;
+ }
+
+ /**
+ * Set the full bounds invalid flag. This flag is set when the full bounds
+ * of this node need to be recomputed as is the case when this node is
+ * transformed or when one of this node's children changes geometry.
+ *
+ * @param fullBoundsInvalid true=invalid, false=valid
+ */
+ protected void setFullBoundsInvalid(final boolean fullBoundsInvalid) {
+ this.fullBoundsInvalid = fullBoundsInvalid;
+ }
+
+ /**
+ * Return true if one of this node's descendants has invalid bounds.
+ *
+ * @return whether child bounds are invalid
+ */
+ protected boolean getChildBoundsInvalid() {
+ return childBoundsInvalid;
+ }
+
+ /**
+ * Set the flag indicating that one of this node's descendants has invalid
+ * bounds.
+ *
+ * @param childBoundsInvalid true=invalid, false=valid
+ */
+ protected void setChildBoundsInvalid(final boolean childBoundsInvalid) {
+ this.childBoundsInvalid = childBoundsInvalid;
+ }
+
+ /**
+ * This method should be called when the bounds of this node are changed. It
+ * invalidates the full bounds of this node, and also notifies each of this
+ * nodes children that their parent's bounds have changed. As a result of
+ * this method getting called this nodes layoutChildren will be called.
+ */
+ public void signalBoundsChanged() {
+ invalidateFullBounds();
+ setBoundsChanged(true);
+ firePropertyChange(PROPERTY_CODE_BOUNDS, PROPERTY_BOUNDS, null, bounds);
+
+ final int count = getChildrenCount();
+ for (int i = 0; i < count; i++) {
+ final PNode each = (PNode) children.get(i);
+ each.parentBoundsChanged();
+ }
+ }
+
+ /**
+ * Invalidate this node's layout, so that later layoutChildren will get
+ * called.
+ */
+ public void invalidateLayout() {
+ invalidateFullBounds();
+ }
+
+ /**
+ * A notification that the bounds of this node's parent have changed.
+ */
+ protected void parentBoundsChanged() {
+ }
+
+ /**
+ * Invalidates the full bounds of this node, and sets the child bounds
+ * invalid flag on each of this node's ancestors.
+ */
+ public void invalidateFullBounds() {
+ setFullBoundsInvalid(true);
+
+ PNode n = parent;
+ while (n != null && !n.getChildBoundsInvalid()) {
+ n.setChildBoundsInvalid(true);
+ n = n.parent;
+ }
+
+ if (SCENE_GRAPH_DELEGATE != null) {
+ SCENE_GRAPH_DELEGATE.nodeFullBoundsInvalidated(this);
+ }
+ }
+
+ /**
+ * This method is called to validate the bounds of this node and all of its
+ * descendants. It returns true if this nodes bounds or the bounds of any of
+ * its descendants are marked as volatile.
+ *
+ * @return true if this node or any of its descendants have volatile bounds
+ */
+ protected boolean validateFullBounds() {
+ final boolean boundsVolatile = getBoundsVolatile();
+
+ // 1. Only compute new bounds if invalid flags are set.
+ if (fullBoundsInvalid || childBoundsInvalid || boundsVolatile || childBoundsVolatile) {
+
+ // 2. If my bounds are volatile and they have not been changed then
+ // signal a change.
+ //
+ // For most cases this will do nothing, but if a nodes bounds depend
+ // on its model, then
+ // validate bounds has the responsibility of making the bounds match
+ // the models value.
+ // For example PPaths validateBounds method makes sure that the
+ // bounds are equal to the
+ // bounds of the GeneralPath model.
+ if (boundsVolatile && !boundsChanged) {
+ signalBoundsChanged();
+ }
+
+ // 3. If the bounds of on of my decendents are invalidate then
+ // validate the bounds of all of my children.
+ if (childBoundsInvalid || childBoundsVolatile) {
+ childBoundsVolatile = false;
+ final int count = getChildrenCount();
+ for (int i = 0; i < count; i++) {
+ if (children.size()<=i) break;
+ final PNode each = (PNode) children.get(i);
+ childBoundsVolatile |= each.validateFullBounds();
+ }
+ }
+
+ // 4. Now that my children's bounds are valid and my own bounds are
+ // valid run any layout algorithm here. Note that if you try to
+ // layout volatile
+ // children piccolo will most likely start a "soft" infinite loop.
+ // It won't freeze
+ // your program, but it will make an infinite number of calls to
+ // SwingUtilities
+ // invoke later. You don't want to do that.
+ layoutChildren();
+
+ // 5. If the full bounds cache is invalid then recompute the full
+ // bounds cache here after our own bounds and the children's bounds
+ // have been computed above.
+ if (fullBoundsInvalid) {
+ final double oldX = fullBoundsCache.x;
+ final double oldY = fullBoundsCache.y;
+ final double oldWidth = fullBoundsCache.width;
+ final double oldHeight = fullBoundsCache.height;
+ final boolean oldEmpty = fullBoundsCache.isEmpty();
+
+ // 6. This will call getFullBoundsReference on all of the
+ // children. So if the above
+ // layoutChildren method changed the bounds of any of the
+ // children they will be
+ // validated again here.
+ fullBoundsCache = computeFullBounds(fullBoundsCache);
+
+ final boolean fullBoundsChanged = fullBoundsCache.x != oldX || fullBoundsCache.y != oldY
+ || fullBoundsCache.width != oldWidth || fullBoundsCache.height != oldHeight
+ || fullBoundsCache.isEmpty() != oldEmpty;
+
+ // 7. If the new full bounds cache differs from the previous
+ // cache then
+ // tell our parent to invalidate their full bounds. This is how
+ // bounds changes
+ // deep in the tree percolate up.
+ if (fullBoundsChanged) {
+ if (parent != null) {
+ parent.invalidateFullBounds();
+ }
+ firePropertyChange(PROPERTY_CODE_FULL_BOUNDS, PROPERTY_FULL_BOUNDS, null, fullBoundsCache);
+
+ // 8. If our paint was invalid make sure to repaint our old
+ // full bounds. The
+ // new bounds will be computed later in the validatePaint
+ // pass.
+ if (paintInvalid && !oldEmpty) {
+ TEMP_REPAINT_BOUNDS.setRect(oldX, oldY, oldWidth, oldHeight);
+ repaintFrom(TEMP_REPAINT_BOUNDS, this);
+ }
+ }
+ }
+
+ // 9. Clear the invalid bounds flags.
+ boundsChanged = false;
+ fullBoundsInvalid = false;
+ childBoundsInvalid = false;
+ }
+
+ return boundsVolatile || childBoundsVolatile;
+ }
+
+ /**
+ * Nodes that apply layout constraints to their children should override
+ * this method and do the layout there.
+ */
+ protected void layoutChildren() {
+ }
+
+ // ****************************************************************
+ // Node Transform - Methods to manipulate the node's transform.
+ //
+ // Each node has a transform that is used to define the nodes
+ // local coordinate system. IE it is applied before picking and
+ // rendering the node.
+ //
+ // The usual way to move nodes about on the canvas is to manipulate
+ // this transform, as opposed to changing the bounds of the
+ // node.
+ //
+ // Since this transform defines the local coordinate system of this
+ // node the following methods with affect the global position both
+ // this node and all of its descendants.
+ // ****************************************************************
+
+ /**
+ * Returns the rotation applied by this node's transform in radians. This
+ * rotation affects this node and all its descendants. The value returned
+ * will be between 0 and 2pi radians.
+ *
+ * @return rotation in radians.
+ */
+ public double getRotation() {
+ if (transform == null) {
+ return 0;
+ }
+ return transform.getRotation();
+ }
+
+ /**
+ * Sets the rotation of this nodes transform in radians. This will affect
+ * this node and all its descendents.
+ *
+ * @param theta rotation in radians
+ */
+ public void setRotation(final double theta) {
+ rotate(theta - getRotation());
+ }
+
+ /**
+ * Rotates this node by theta (in radians) about the 0,0 point. This will
+ * affect this node and all its descendants.
+ *
+ * @param theta the amount to rotate by in radians
+ */
+ public void rotate(final double theta) {
+ rotateAboutPoint(theta, 0, 0);
+ }
+
+ /**
+ * Rotates this node by theta (in radians), and then translates the node so
+ * that the x, y position of its fullBounds stays constant.
+ *
+ * @param theta the amount to rotate by in radians
+ */
+ public void rotateInPlace(final double theta) {
+ PBounds b = getFullBoundsReference();
+ final double px = b.x;
+ final double py = b.y;
+ rotateAboutPoint(theta, 0, 0);
+ b = getFullBoundsReference();
+ offset(px - b.x, py - b.y);
+ }
+
+ /**
+ * Rotates this node by theta (in radians) about the given point. This will
+ * affect this node and all its descendants.
+ *
+ * @param theta the amount to rotate by in radians
+ * @param point the point about which to rotate
+ */
+ public void rotateAboutPoint(final double theta, final Point2D point) {
+ rotateAboutPoint(theta, point.getX(), point.getY());
+ }
+
+ /**
+ * Rotates this node by theta (in radians) about the given point. This will
+ * affect this node and all its descendants.
+ *
+ * @param theta the amount to rotate by in radians
+ * @param x the x coordinate of the point around which to rotate
+ * @param y the y coordinate of the point around which to rotate
+ */
+ public void rotateAboutPoint(final double theta, final double x, final double y) {
+ getTransformReference(true).rotate(theta, x, y);
+ invalidatePaint();
+ invalidateFullBounds();
+ firePropertyChange(PROPERTY_CODE_TRANSFORM, PROPERTY_TRANSFORM, null, transform);
+ }
+
+ /**
+ * Return the total amount of rotation applied to this node by its own
+ * transform together with the transforms of all its ancestors. The value
+ * returned will be between 0 and 2pi radians.
+ *
+ * @return the total amount of rotation applied to this node in radians
+ */
+ public double getGlobalRotation() {
+ return getLocalToGlobalTransform(null).getRotation();
+ }
+
+ /**
+ * Set the global rotation (in radians) of this node. This is implemented by
+ * rotating this nodes transform the required amount so that the nodes
+ * global rotation is as requested.
+ *
+ * @param theta the amount to rotate by in radians relative to the global
+ * coordinate system.
+ */
+ public void setGlobalRotation(final double theta) {
+ if (parent != null) {
+ setRotation(theta - parent.getGlobalRotation());
+ }
+ else {
+ setRotation(theta);
+ }
+ }
+
+ /**
+ * Return the scale applied by this node's transform. The scale is effecting
+ * this node and all its descendants.
+ *
+ * @return scale applied by this nodes transform.
+ */
+ public double getScale() {
+ if (transform == null) {
+ return 1;
+ }
+ return transform.getScale();
+ }
+
+ /**
+ * Set the scale of this node's transform. The scale will affect this node
+ * and all its descendants.
+ *
+ * @param scale the scale to set the transform to
+ */
+ public void setScale(final double scale) {
+ if (scale == 0) {
+ throw new RuntimeException("Can't set scale to 0");
+ }
+ scale(scale / getScale());
+ }
+
+ /**
+ * Scale this nodes transform by the given amount. This will affect this
+ * node and all of its descendants.
+ *
+ * @param scale the amount to scale by
+ */
+ public void scale(final double scale) {
+ scaleAboutPoint(scale, 0, 0);
+ }
+
+ /**
+ * Scale this nodes transform by the given amount about the specified point.
+ * This will affect this node and all of its descendants.
+ *
+ * @param scale the amount to scale by
+ * @param point the point to scale about
+ */
+ public void scaleAboutPoint(final double scale, final Point2D point) {
+ scaleAboutPoint(scale, point.getX(), point.getY());
+ }
+
+ /**
+ * Scale this nodes transform by the given amount about the specified point.
+ * This will affect this node and all of its descendants.
+ *
+ * @param scale the amount to scale by
+ * @param x the x coordinate of the point around which to scale
+ * @param y the y coordinate of the point around which to scale
+ */
+ public void scaleAboutPoint(final double scale, final double x, final double y) {
+ getTransformReference(true).scaleAboutPoint(scale, x, y);
+ invalidatePaint();
+ invalidateFullBounds();
+ firePropertyChange(PROPERTY_CODE_TRANSFORM, PROPERTY_TRANSFORM, null, transform);
+ }
+
+ /**
+ * Return the global scale that is being applied to this node by its
+ * transform together with the transforms of all its ancestors.
+ *
+ * @return global scale of this node
+ */
+ public double getGlobalScale() {
+ return getLocalToGlobalTransform(null).getScale();
+ }
+
+ /**
+ * Set the global scale of this node. This is implemented by scaling this
+ * nodes transform the required amount so that the nodes global scale is as
+ * requested.
+ *
+ * @param scale the desired global scale
+ */
+ public void setGlobalScale(final double scale) {
+ if (parent != null) {
+ setScale(scale / parent.getGlobalScale());
+ }
+ else {
+ setScale(scale);
+ }
+ }
+
+ /**
+ * Returns the x offset of this node as applied by its transform.
+ *
+ * @return x offset of this node as applied by its transform
+ */
+ public double getXOffset() {
+ if (transform == null) {
+ return 0;
+ }
+ return transform.getTranslateX();
+ }
+
+ /**
+ * Returns the y offset of this node as applied by its transform.
+ *
+ * @return y offset of this node as applied by its transform
+ */
+ public double getYOffset() {
+ if (transform == null) {
+ return 0;
+ }
+ return transform.getTranslateY();
+ }
+
+ /**
+ * Return the offset that is being applied to this node by its transform.
+ * This offset effects this node and all of its descendants and is specified
+ * in the parent coordinate system. This returns the values that are in the
+ * m02 and m12 positions in the affine transform.
+ *
+ * @return a point representing the x and y offset
+ */
+ public Point2D getOffset() {
+ if (transform == null) {
+ return new Point2D.Double();
+ }
+ return new Point2D.Double(transform.getTranslateX(), transform.getTranslateY());
+ }
+
+ /**
+ * Set the offset that is being applied to this node by its transform. This
+ * offset effects this node and all of its descendants and is specified in
+ * the nodes parent coordinate system. This directly sets the values of the
+ * m02 and m12 positions in the affine transform. Unlike "PNode.translate()"
+ * it is not effected by the transforms scale.
+ *
+ * @param point value of new offset
+ */
+ public void setOffset(final Point2D point) {
+ setOffset(point.getX(), point.getY());
+ }
+
+ /**
+ * Set the offset that is being applied to this node by its transform. This
+ * offset effects this node and all of its descendants and is specified in
+ * the nodes parent coordinate system. This directly sets the values of the
+ * m02 and m12 positions in the affine transform. Unlike "PNode.translate()"
+ * it is not effected by the transforms scale.
+ *
+ * @param x amount of x offset
+ * @param y amount of y offset
+ */
+ public void setOffset(final double x, final double y) {
+ getTransformReference(true).setOffset(x, y);
+ invalidatePaint();
+ invalidateFullBounds();
+ firePropertyChange(PROPERTY_CODE_TRANSFORM, PROPERTY_TRANSFORM, null, transform);
+ }
+
+ /**
+ * Offset this node relative to the parents coordinate system, and is NOT
+ * effected by this nodes current scale or rotation. This is implemented by
+ * directly adding dx to the m02 position and dy to the m12 position in the
+ * affine transform.
+ *
+ * @param dx amount to add to this nodes current x Offset
+ * @param dy amount to add to this nodes current y Offset
+ */
+ public void offset(final double dx, final double dy) {
+ getTransformReference(true);
+ setOffset(transform.getTranslateX() + dx, transform.getTranslateY() + dy);
+ }
+
+ /**
+ * Translate this node's transform by the given amount, using the standard
+ * affine transform translate method. This translation effects this node and
+ * all of its descendants.
+ *
+ * @param dx amount to add to this nodes current x translation
+ * @param dy amount to add to this nodes current y translation
+ */
+ public void translate(final double dx, final double dy) {
+ getTransformReference(true).translate(dx, dy);
+ invalidatePaint();
+ invalidateFullBounds();
+ firePropertyChange(PROPERTY_CODE_TRANSFORM, PROPERTY_TRANSFORM, null, transform);
+ }
+
+ /**
+ * Return the global translation that is being applied to this node by its
+ * transform together with the transforms of all its ancestors.
+ *
+ * @return the global translation applied to this node
+ */
+ public Point2D getGlobalTranslation() {
+ final Point2D p = getOffset();
+ if (parent != null) {
+ parent.localToGlobal(p);
+ }
+ return p;
+ }
+
+ /**
+ * Set the global translation of this node. This is implemented by
+ * translating this nodes transform the required amount so that the nodes
+ * global scale is as requested.
+ *
+ * @param globalPoint the desired global translation
+ */
+ public void setGlobalTranslation(final Point2D globalPoint) {
+ if (parent != null) {
+ parent.getGlobalToLocalTransform(null).transform(globalPoint, globalPoint);
+ }
+ setOffset(globalPoint);
+ }
+
+ /**
+ * Transform this nodes transform by the given transform.
+ *
+ * @param aTransform the transform to apply.
+ */
+ public void transformBy(final AffineTransform aTransform) {
+ getTransformReference(true).concatenate(aTransform);
+ invalidatePaint();
+ invalidateFullBounds();
+ firePropertyChange(PROPERTY_CODE_TRANSFORM, PROPERTY_TRANSFORM, null, transform);
+ }
+
+ /**
+ * Linearly interpolates between a and b, based on t. Specifically, it
+ * computes lerp(a, b, t) = a + t*(b - a). This produces a result that
+ * changes from a (when t = 0) to b (when t = 1).
+ *
+ * @param t variable 'time' parameter
+ * @param a from point
+ * @param b to Point
+ *
+ * @return linear interpolation between and b at time interval t (given as #
+ * between 0f and 1f)
+ */
+ public static double lerp(final double t, final double a, final double b) {
+ return a + t * (b - a);
+ }
+
+ /**
+ * This will calculate the necessary transform in order to make this node
+ * appear at a particular position relative to the specified bounding box.
+ * The source point specifies a point in the unit square (0, 0) - (1, 1)
+ * that represents an anchor point on the corresponding node to this
+ * transform. The destination point specifies an anchor point on the
+ * reference node. The position method then computes the transform that
+ * results in transforming this node so that the source anchor point
+ * coincides with the reference anchor point. This can be useful for layout
+ * algorithms as it is straightforward to position one object relative to
+ * another.
+ *
+ * For example, If you have two nodes, A and B, and you call + * + *
+ * Point2D srcPt = new Point2D.Double(1.0, 0.0); + * Point2D destPt = new Point2D.Double(0.0, 0.0); + * A.position(srcPt, destPt, B.getGlobalBounds(), 750, null); + *+ * + * The result is that A will move so that its upper-right corner is at the + * same place as the upper-left corner of B, and the transition will be + * smoothly animated over a period of 750 milliseconds. + * + * @since 1.3 + * @param srcPt The anchor point on this transform's node (normalized to a + * unit square) + * @param destPt The anchor point on destination bounds (normalized to a + * unit square) + * @param destBounds The bounds (in global coordinates) used to calculate + * this transform's node + * @param millis Number of milliseconds over which to perform the animation + * + * @return newly scheduled activity or node if activity could not be + * scheduled + */ + public PActivity animateToRelativePosition(final Point2D srcPt, final Point2D destPt, final Rectangle2D destBounds, + final int millis) { + double srcx, srcy; + double destx, desty; + double dx, dy; + Point2D pt1, pt2; + + if (parent == null) { + return null; + } + else { + // First compute translation amount in global coordinates + final Rectangle2D srcBounds = getGlobalFullBounds(); + srcx = lerp(srcPt.getX(), srcBounds.getX(), srcBounds.getX() + srcBounds.getWidth()); + srcy = lerp(srcPt.getY(), srcBounds.getY(), srcBounds.getY() + srcBounds.getHeight()); + destx = lerp(destPt.getX(), destBounds.getX(), destBounds.getX() + destBounds.getWidth()); + desty = lerp(destPt.getY(), destBounds.getY(), destBounds.getY() + destBounds.getHeight()); + + // Convert vector to local coordinates + pt1 = new Point2D.Double(srcx, srcy); + globalToLocal(pt1); + pt2 = new Point2D.Double(destx, desty); + globalToLocal(pt2); + dx = pt2.getX() - pt1.getX(); + dy = pt2.getY() - pt1.getY(); + + // Finally, animate change + final PAffineTransform at = new PAffineTransform(getTransformReference(true)); + at.translate(dx, dy); + return animateToTransform(at, millis); + } + } + + /** + * @deprecated in favor of animateToRelativePosition + * + * It will calculate the necessary transform in order to make + * this node appear at a particular position relative to the + * specified bounding box. The source point specifies a point in + * the unit square (0, 0) - (1, 1) that represents an anchor + * point on the corresponding node to this transform. The + * destination point specifies an anchor point on the reference + * node. The position method then computes the transform that + * results in transforming this node so that the source anchor + * point coincides with the reference anchor point. This can be + * useful for layout algorithms as it is straightforward to + * position one object relative to another. + *
+ * For example, If you have two nodes, A and B, and you call + * + *
+ * Point2D srcPt = new Point2D.Double(1.0, 0.0); + * Point2D destPt = new Point2D.Double(0.0, 0.0); + * A.position(srcPt, destPt, B.getGlobalBounds(), 750, null); + *+ * + * The result is that A will move so that its upper-right corner + * is at the same place as the upper-left corner of B, and the + * transition will be smoothly animated over a period of 750 + * milliseconds. + * + * @param srcPt The anchor point on this transform's node (normalized to a + * unit square) + * @param destPt The anchor point on destination bounds (normalized to a + * unit square) + * @param destBounds The bounds (in global coordinates) used to calculate + * this transform's node + * @param millis Number of milliseconds over which to perform the animation + */ + public void position(final Point2D srcPt, final Point2D destPt, final Rectangle2D destBounds, final int millis) { + animateToRelativePosition(srcPt, destPt, destBounds, millis); + }; + + /** + * Return a copy of the transform associated with this node. + * + * @return copy of this node's transform + */ + public PAffineTransform getTransform() { + if (transform == null) { + return new PAffineTransform(); + } + else { + return (PAffineTransform) transform.clone(); + } + } + + /** + * Return a reference to the transform associated with this node. This + * returned transform should not be modified. PNode transforms are created + * lazily when needed. If you access the transform reference before the + * transform has been created it may return null. The + * createNewTransformIfNull parameter is used to specify that the PNode + * should create a new transform (and assign that transform to the nodes + * local transform variable) instead of returning null. + * + * @param createNewTransformIfNull if the transform has not been + * initialised, should it be? + * + * @return reference to this node's transform + */ + public PAffineTransform getTransformReference(final boolean createNewTransformIfNull) { + if (transform == null && createNewTransformIfNull) { + transform = new PAffineTransform(); + } + return transform; + } + + /** + * Return an inverted copy of the transform associated with this node. + * + * @return inverted copy of this node's transform + */ + public PAffineTransform getInverseTransform() { + if (transform == null) { + return new PAffineTransform(); + } + + try { + return new PAffineTransform(transform.createInverse()); + } + catch (final NoninvertibleTransformException e) { + throw new PAffineTransformException(e, transform); + } + } + + /** + * Set the transform applied to this node. + * + * @param transform the new transform value + */ + public void setTransform(final AffineTransform transform) { + if (transform == null) { + this.transform = null; + } + else { + getTransformReference(true).setTransform(transform); + } + + invalidatePaint(); + invalidateFullBounds(); + firePropertyChange(PROPERTY_CODE_TRANSFORM, PROPERTY_TRANSFORM, null, this.transform); + } + + // **************************************************************** + // Paint Damage Management - Methods used to invalidate the areas of + // the screen that this node appears in so that they will later get + // painted. + // + // Generally you will not need to call these invalidate methods + // when starting out with Piccolo2d because methods such as setPaint + // already automatically call them for you. You will need to call + // them when you start creating your own nodes. + // + // When you do create you own nodes the only method that you will + // normally need to call is invalidatePaint. This method marks the + // nodes as having invalid paint, the root node's UI cycle will then + // later discover this damage and report it to the Java repaint manager. + // + // Repainting is normally done with PNode.invalidatePaint() instead of + // directly calling PNode.repaint() because PNode.repaint() requires + // the nodes bounds to be computed right away. But with invalidatePaint + // the bounds computation can be delayed until the end of the root's UI + // cycle, and this can add up to a bit savings when modifying a + // large number of nodes all at once. + // + // The other methods here will rarely be called except internally + // from the framework. + // **************************************************************** + + /** + * Return true if this nodes paint is invalid, in which case the node needs + * to be repainted. + * + * @return true if this node needs to be repainted + */ + public boolean getPaintInvalid() { + return paintInvalid; + } + + /** + * Mark this node as having invalid paint. If this is set the node will + * later be repainted. Node this method is most often used internally. + * + * @param paintInvalid true if this node should be repainted + */ + public void setPaintInvalid(final boolean paintInvalid) { + this.paintInvalid = paintInvalid; + } + + /** + * Return true if this node has a child with invalid paint. + * + * @return true if this node has a child with invalid paint + */ + public boolean getChildPaintInvalid() { + return childPaintInvalid; + } + + /** + * Mark this node as having a child with invalid paint. + * + * @param childPaintInvalid true if this node has a child with invalid paint + */ + public void setChildPaintInvalid(final boolean childPaintInvalid) { + this.childPaintInvalid = childPaintInvalid; + } + + /** + * Invalidate this node's paint, and mark all of its ancestors as having a + * node with invalid paint. + */ + public void invalidatePaint() { + setPaintInvalid(true); + + PNode n = parent; + while (n != null && !n.getChildPaintInvalid()) { + n.setChildPaintInvalid(true); + n = n.parent; + } + + if (SCENE_GRAPH_DELEGATE != null) { + SCENE_GRAPH_DELEGATE.nodePaintInvalidated(this); + } + } + + /** + * Repaint this node and any of its descendants if they have invalid paint. + */ + public void validateFullPaint() { + if (getPaintInvalid()) { + repaint(); + setPaintInvalid(false); + } + + if (getChildPaintInvalid()) { + final int count = getChildrenCount(); + for (int i = 0; i < count; i++) { + final PNode each = (PNode) children.get(i); + each.validateFullPaint(); + } + setChildPaintInvalid(false); + } + } + + /** + * Mark the area on the screen represented by this nodes full bounds as + * needing a repaint. + */ + public void repaint() { + TEMP_REPAINT_BOUNDS.setRect(getFullBoundsReference()); + repaintFrom(TEMP_REPAINT_BOUNDS, this); + } + + /** + * Pass the given repaint request up the tree, so that any cameras can + * invalidate that region on their associated canvas. + * + * @param localBounds the bounds to repaint + * @param childOrThis if childOrThis does not equal this then this nodes + * transform will be applied to the localBounds param + */ + public void repaintFrom(final PBounds localBounds, final PNode childOrThis) { + if (parent != null) { + if (childOrThis != this) { + localToParent(localBounds); + } + else if (!getVisible()) { + return; + } + parent.repaintFrom(localBounds, this); + } + } + + // **************************************************************** + // Occluding - Methods to support occluding optimisation. Not yet + // complete. + // **************************************************************** + + /** + * Returns whether this node is Opaque. + * + * @param boundary boundary to check and see if this node covers completely. + * + * @return true if opaque + */ + public boolean isOpaque(final Rectangle2D boundary) { + return false; + } + + /** + * Returns whether this node has been flagged as occluded. + * + * @return true if occluded + */ + public boolean getOccluded() { + return occluded; + } + + /** + * Flags this node as occluded. + * + * @param occluded new value for occluded + */ + public void setOccluded(final boolean occluded) { + this.occluded = occluded; + } + + // **************************************************************** + // Painting - Methods for painting this node and its children + // + // Painting is how a node defines its visual representation on the + // screen, and is done in the local coordinate system of the node. + // + // The default painting behavior is to first paint the node, and + // then paint the node's children on top of the node. If a node + // needs wants specialised painting behavior it can override: + // + // paint() - Painting here will happen before the children + // are painted, so the children will be painted on top of painting done + // here. + // paintAfterChildren() - Painting here will happen after the children + // are painted, so it will paint on top of them. + // + // Note that you should not normally need to override fullPaint(). + // + // The visible flag can be used to make a node invisible so that + // it will never get painted. + // **************************************************************** + + /** + * Return true if this node is visible, that is if it will paint itself and + * descendants. + * + * @return true if this node and its descendants are visible. + */ + public boolean getVisible() { + return visible; + } + + /** + * Set the visibility of this node and its descendants. + * + * @param isVisible true if this node and its descendants are visible + */ + public void setVisible(final boolean isVisible) { + if (getVisible() != isVisible) { + if (!isVisible) { + repaint(); + } + visible = isVisible; + firePropertyChange(PROPERTY_CODE_VISIBLE, PROPERTY_VISIBLE, null, null); + invalidatePaint(); + } + } + + /** + * Return the paint used while painting this node. This value may be null. + * + * @return the paint used while painting this node. + */ + public Paint getPaint() { + return paint; + } + + /** + * Set the paint used to paint this node, which may be null. + * + * @param newPaint paint that this node should use when painting itself. + */ + public void setPaint(final Paint newPaint) { + if (paint == newPaint) { + return; + } + + final Paint oldPaint = paint; + paint = newPaint; + invalidatePaint(); + firePropertyChange(PROPERTY_CODE_PAINT, PROPERTY_PAINT, oldPaint, paint); + } + + /** + * Return the transparency used when painting this node. Note that this + * transparency is also applied to all of the node's descendants. + * + * @return how transparent this node is 0f = completely transparent, 1f = + * completely opaque + */ + public float getTransparency() { + return transparency; + } + + /** + * Set the transparency used to paint this node. Note that this transparency + * applies to this node and all of its descendants. + * + * @param newTransparency transparency value for this node. 0f = fully + * transparent, 1f = fully opaque + */ + public void setTransparency(final float newTransparency) { + if (Math.abs(transparency - newTransparency) > TRANSPARENCY_RESOLUTION) { + final float oldTransparency = transparency; + transparency = newTransparency; + invalidatePaint(); + firePropertyChange(PROPERTY_CODE_TRANSPARENCY, PROPERTY_TRANSPARENCY, new Float(oldTransparency), + new Float(newTransparency)); + } + } + + /** + * Paint this node behind any of its children nodes. Subclasses that define + * a different appearance should override this method and paint themselves + * there. + * + * @param paintContext the paint context to use for painting the node + */ + protected void paint(final PPaintContext paintContext) { + if (paint != null) { + final Graphics2D g2 = paintContext.getGraphics(); + g2.setPaint(paint); + g2.fill(getBoundsReference()); + } + } + + /** + * Paint this node and all of its descendants. Most subclasses do not need + * to override this method, they should override
paint
or
+ * paintAfterChildren
instead.
+ *
+ * @param paintContext the paint context to use for painting this node and
+ * its children
+ */
+ public void fullPaint(final PPaintContext paintContext) {
+ if (getVisible() && fullIntersects(paintContext.getLocalClip())) {
+ paintContext.pushTransform(transform);
+ paintContext.pushTransparency(transparency);
+
+ if (!getOccluded()) {
+ paint(paintContext);
+ }
+
+ final int count = getChildrenCount();
+ for (int i = 0; i < count; i++) {
+ final PNode each = (PNode) children.get(i);
+ each.fullPaint(paintContext);
+ }
+
+ paintAfterChildren(paintContext);
+
+ paintContext.popTransparency(transparency);
+ paintContext.popTransform(transform);
+ }
+ }
+
+ /**
+ * Subclasses that wish to do additional painting after their children are
+ * painted should override this method and do that painting here.
+ *
+ * @param paintContext the paint context to sue for painting after the
+ * children are painted
+ */
+ protected void paintAfterChildren(final PPaintContext paintContext) {
+ }
+
+ /**
+ * Return a new Image representing this node and all of its children. The
+ * image size will be equal to the size of this nodes full bounds.
+ *
+ * @return a new image representing this node and its descendants
+ */
+ public Image toImage() {
+ final PBounds b = getFullBoundsReference();
+ return toImage((int) Math.ceil(b.getWidth()), (int) Math.ceil(b.getHeight()), null);
+ }
+
+ /**
+ * Return a new Image of the requested size representing this node and all
+ * of its children. If backGroundPaint is null the resulting image will have
+ * transparent regions, otherwise those regions will be filled with the
+ * backgroundPaint.
+ *
+ * @param width pixel width of the resulting image
+ * @param height pixel height of the resulting image
+ * @param backgroundPaint paint to fill the image with before drawing this
+ * node, may be null
+ *
+ * @return a new image representing this node and its descendants
+ */
+ public Image toImage(final int width, final int height, final Paint backgroundPaint) {
+ BufferedImage result;
+
+ if (GraphicsEnvironment.isHeadless()) {
+ result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ }
+ else {
+ final GraphicsConfiguration graphicsConfiguration = GraphicsEnvironment.getLocalGraphicsEnvironment()
+ .getDefaultScreenDevice().getDefaultConfiguration();
+ result = graphicsConfiguration.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
+ }
+
+ return toImage(result, backgroundPaint);
+ }
+
+ /**
+ * Paint a representation of this node into the specified buffered image. If
+ * background, paint is null, then the image will not be filled with a color
+ * prior to rendering
+ *
+ * @param image Image onto which this node will be painted
+ * @param backGroundPaint will fill background of image with this. May be
+ * null.
+ * @return a rendering of this image and its descendants onto the specified
+ * image
+ */
+ public Image toImage(final BufferedImage image, final Paint backGroundPaint) {
+ return toImage(image, backGroundPaint, FILL_STRATEGY_ASPECT_FIT);
+ }
+
+ /**
+ * Paint a representation of this node into the specified buffered image. If
+ * background, paint is null, then the image will not be filled with a color
+ * prior to rendering
+ *
+ * @since 1.3
+ * @param image Image onto which this node will be painted
+ * @param backGroundPaint will fill background of image with this. May be
+ * null.
+ * @param fillStrategy strategy to use regarding how node will cover the
+ * image
+ * @return a rendering of this image and its descendants onto the specified
+ * image
+ */
+ public Image toImage(final BufferedImage image, final Paint backGroundPaint, final int fillStrategy) {
+ final int imageWidth = image.getWidth();
+ final int imageHeight = image.getHeight();
+ final Graphics2D g2 = image.createGraphics();
+
+ if (backGroundPaint != null) {
+ g2.setPaint(backGroundPaint);
+ g2.fillRect(0, 0, imageWidth, imageHeight);
+ }
+ g2.setClip(0, 0, imageWidth, imageHeight);
+
+ final PBounds nodeBounds = getFullBounds();
+ nodeBounds.expandNearestIntegerDimensions();
+
+ final double nodeWidth = nodeBounds.getWidth();
+ final double nodeHeight = nodeBounds.getHeight();
+
+ double imageRatio = imageWidth / (imageHeight * 1.0);
+ double nodeRatio = nodeWidth / nodeHeight;
+ double scale;
+ switch (fillStrategy) {
+ case FILL_STRATEGY_ASPECT_FIT:
+ // scale the graphics so node's full bounds fit in the imageable
+ // bounds but aspect ration is retained
+
+ if (nodeRatio <= imageRatio) {
+ scale = image.getHeight() / nodeHeight;
+ }
+ else {
+ scale = image.getWidth() / nodeWidth;
+ }
+ g2.scale(scale, scale);
+ g2.translate(-nodeBounds.x, -nodeBounds.y);
+ break;
+ case FILL_STRATEGY_ASPECT_COVER:
+ // scale the graphics so node completely covers the imageable
+ // area, but retains its aspect ratio.
+ if (nodeRatio <= imageRatio) {
+ scale = image.getWidth() / nodeWidth;
+ }
+ else {
+ scale = image.getHeight() / nodeHeight;
+ }
+ g2.scale(scale, scale);
+ break;
+ case FILL_STRATEGY_EXACT_FIT:
+ // scale the node so that it covers then entire image,
+ // distorting it if necessary.
+ g2.scale(image.getWidth() / nodeWidth, image.getHeight() / nodeHeight);
+ g2.translate(-nodeBounds.x, -nodeBounds.y);
+ break;
+ default:
+ throw new IllegalArgumentException("Fill strategy provided is invalid");
+ }
+
+ final PPaintContext pc = new PPaintContext(g2);
+ pc.setRenderQuality(PPaintContext.HIGH_QUALITY_RENDERING);
+ fullPaint(pc);
+ return image;
+ }
+
+ /**
+ * Constructs a new PrinterJob, allows the user to select which printer to
+ * print to, And then prints the node.
+ */
+ public void print() {
+ final PrinterJob printJob = PrinterJob.getPrinterJob();
+ final PageFormat pageFormat = printJob.defaultPage();
+ final Book book = new Book();
+ book.append(this, pageFormat);
+ printJob.setPageable(book);
+
+ if (printJob.printDialog()) {
+ try {
+ printJob.print();
+ }
+ catch (final PrinterException e) {
+ throw new RuntimeException("Error Printing", e);
+ }
+ }
+ }
+
+ /**
+ * Prints the node into the given Graphics context using the specified
+ * format. The zero based index of the requested page is specified by
+ * pageIndex. If the requested page does not exist then this method returns
+ * NO_SUCH_PAGE; otherwise PAGE_EXISTS is returned. If the printable object
+ * aborts the print job then it throws a PrinterException.
+ *
+ * @param graphics the context into which the node is drawn
+ * @param pageFormat the size and orientation of the page
+ * @param pageIndex the zero based index of the page to be drawn
+ *
+ * @return Either NO_SUCH_PAGE or PAGE_EXISTS
+ */
+ public int print(final Graphics graphics, final PageFormat pageFormat, final int pageIndex) {
+ if (pageIndex != 0) {
+ return NO_SUCH_PAGE;
+ }
+
+ if (!(graphics instanceof Graphics2D)) {
+ throw new IllegalArgumentException("Provided graphics context is not a Graphics2D object");
+ }
+
+ final Graphics2D g2 = (Graphics2D) graphics;
+ final PBounds imageBounds = getFullBounds();
+
+ imageBounds.expandNearestIntegerDimensions();
+
+ g2.setClip(0, 0, (int) pageFormat.getWidth(), (int) pageFormat.getHeight());
+ g2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
+
+ // scale the graphics so node's full bounds fit in the imageable bounds.
+ double scale = pageFormat.getImageableWidth() / imageBounds.getWidth();
+ if (pageFormat.getImageableHeight() / imageBounds.getHeight() < scale) {
+ scale = pageFormat.getImageableHeight() / imageBounds.getHeight();
+ }
+
+ g2.scale(scale, scale);
+ g2.translate(-imageBounds.x, -imageBounds.y);
+
+ final PPaintContext pc = new PPaintContext(g2);
+ pc.setRenderQuality(PPaintContext.HIGH_QUALITY_RENDERING);
+ fullPaint(pc);
+
+ return PAGE_EXISTS;
+ }
+
+ // ****************************************************************
+ // Picking - Methods for picking this node and its children.
+ //
+ // Picking is used to determine the node that intersects a point or
+ // rectangle on the screen. It is most frequently used by the
+ // PInputManager to determine the node that the cursor is over.
+ //
+ // The intersects() method is used to determine if a node has
+ // been picked or not. The default implementation just test to see
+ // if the pick bounds intersects the bounds of the node. Subclasses
+ // whose geometry (a circle for example) does not match up exactly with
+ // the bounds should override the intersects() method.
+ //
+ // The default picking behavior is to first try to pick the nodes
+ // children, and then try to pick the nodes own bounds. If a node
+ // wants specialized picking behavior it can override:
+ //
+ // pick() - Pick nodes here that should be picked before the nodes
+ // children are picked.
+ // pickAfterChildren() - Pick nodes here that should be picked after the
+ // node's children are picked.
+ //
+ // Note that fullPick should not normally be overridden.
+ //
+ // The pickable and childrenPickable flags can be used to make a
+ // node or it children not pickable even if their geometry does
+ // intersect the pick bounds.
+ // ****************************************************************
+
+ /**
+ * Return true if this node is pickable. Only pickable nodes can receive
+ * input events. Nodes are pickable by default.
+ *
+ * @return true if this node is pickable
+ */
+ public boolean getPickable() {
+ return pickable;
+ }
+
+ /**
+ * Set the pickable flag for this node. Only pickable nodes can receive
+ * input events. Nodes are pickable by default.
+ *
+ * @param isPickable true if this node is pickable
+ */
+ public void setPickable(final boolean isPickable) {
+ if (getPickable() != isPickable) {
+ pickable = isPickable;
+ firePropertyChange(PROPERTY_CODE_PICKABLE, PROPERTY_PICKABLE, null, null);
+ }
+ }
+
+ /**
+ * Return true if the children of this node should be picked. If this flag
+ * is false then this node will not try to pick its children. Children are
+ * pickable by default.
+ *
+ * @return true if this node tries to pick its children
+ */
+ public boolean getChildrenPickable() {
+ return childrenPickable;
+ }
+
+ /**
+ * Set the children pickable flag. If this flag is false then this node will
+ * not try to pick its children. Children are pickable by default.
+ *
+ * @param areChildrenPickable true if this node tries to pick its children
+ */
+ public void setChildrenPickable(final boolean areChildrenPickable) {
+ if (getChildrenPickable() != areChildrenPickable) {
+ childrenPickable = areChildrenPickable;
+ firePropertyChange(PROPERTY_CODE_CHILDREN_PICKABLE, PROPERTY_CHILDREN_PICKABLE, null, null);
+ }
+ }
+
+ /**
+ * Try to pick this node before its children have had a chance to be picked.
+ * Nodes that paint on top of their children may want to override this
+ * method to if the pick path intersects that paint.
+ *
+ * @param pickPath the pick path used for the pick operation
+ * @return true if this node was picked
+ */
+ protected boolean pick(final PPickPath pickPath) {
+ return false;
+ }
+
+ /**
+ * Try to pick this node and all of its descendants. Most subclasses should
+ * not need to override this method. Instead they should override
+ * pick
or pickAfterChildren
.
+ *
+ * @param pickPath the pick path to add the node to if its picked
+ * @return true if this node or one of its descendants was picked.
+ */
+ public boolean fullPick(final PPickPath pickPath) {
+ if (getVisible() && (getPickable() || getChildrenPickable()) && fullIntersects(pickPath.getPickBounds())) {
+ pickPath.pushNode(this);
+ pickPath.pushTransform(transform);
+
+ final boolean thisPickable = getPickable() && pickPath.acceptsNode(this);
+
+ if (thisPickable && pick(pickPath)) {
+ return true;
+ }
+
+ if (getChildrenPickable()) {
+ final int count = getChildrenCount();
+ for (int i = count - 1; i >= 0; i--) {
+ final PNode each = (PNode) children.get(i);
+ if (each.fullPick(pickPath)) {
+ return true;
+ }
+ }
+ }
+
+ if (thisPickable && pickAfterChildren(pickPath)) {
+ return true;
+ }
+
+ pickPath.popTransform(transform);
+ pickPath.popNode(this);
+ }
+
+ return false;
+ }
+
+ /**
+ * Finds all descendants of this node that intersect with the given bounds
+ * and adds them to the results array.
+ *
+ * @param fullBounds bounds to compare against
+ * @param results array into which to add matches
+ */
+ public void findIntersectingNodes(final Rectangle2D fullBounds, final ArrayList results) {
+ if (fullIntersects(fullBounds)) {
+ final Rectangle2D localBounds = parentToLocal((Rectangle2D) fullBounds.clone());
+
+ if (intersects(localBounds)) {
+ results.add(this);
+ }
+
+ final int count = getChildrenCount();
+ for (int i = count - 1; i >= 0; i--) {
+ final PNode each = (PNode) children.get(i);
+ each.findIntersectingNodes(localBounds, results);
+ }
+ }
+ }
+
+ /**
+ * Try to pick this node after its children have had a chance to be picked.
+ * Most subclasses the define a different geometry will need to override
+ * this method.
+ *
+ * @param pickPath the pick path used for the pick operation
+ * @return true if this node was picked
+ */
+ protected boolean pickAfterChildren(final PPickPath pickPath) {
+ if (intersects(pickPath.getPickBounds())) {
+ return true;
+ }
+ return false;
+ }
+
+ // ****************************************************************
+ // Structure - Methods for manipulating and traversing the
+ // parent child relationship
+ //
+ // Most of these methods won't need to be overridden by subclasses
+ // but you will use them frequently to build up your node structures.
+ // ****************************************************************
+
+ /**
+ * Add a node to be a new child of this node. The new node is added to the
+ * end of the list of this node's children. If child was previously a child
+ * of another node, it is removed from that first.
+ *
+ * @param child the new child to add to this node
+ */
+ public void addChild(final PNode child) {
+ int insertIndex = getChildrenCount();
+ if (child.parent == this) {
+ insertIndex--;
+ }
+ addChild(insertIndex, child);
+ }
+
+ /**
+ * Add a node to be a new child of this node at the specified index. If
+ * child was previously a child of another node, it is removed from that
+ * node first.
+ *
+ * @param index where in the children list to insert the child
+ * @param child the new child to add to this node
+ */
+ public void addChild(final int index, final PNode child) {
+ final PNode oldParent = child.getParent();
+
+ if (oldParent != null) {
+ oldParent.removeChild(child);
+ }
+
+ child.setParent(this);
+ getChildrenReference().add(index, child);
+ child.invalidatePaint();
+ invalidateFullBounds();
+
+ firePropertyChange(PROPERTY_CODE_CHILDREN, PROPERTY_CHILDREN, null, children);
+ }
+
+ /**
+ * Add a collection of nodes to be children of this node. If these nodes
+ * already have parents they will first be removed from those parents.
+ *
+ * @param nodes a collection of nodes to be added to this node
+ */
+ public void addChildren(final Collection nodes) {
+ final Iterator i = nodes.iterator();
+ while (i.hasNext()) {
+ final PNode each = (PNode) i.next();
+ addChild(each);
+ }
+ }
+
+ /**
+ * Return true if this node is an ancestor of the parameter node.
+ *
+ * @param node a possible descendant node
+ * @return true if this node is an ancestor of the given node
+ */
+ public boolean isAncestorOf(final PNode node) {
+ PNode p = node.parent;
+ while (p != null) {
+ if (p == this) {
+ return true;
+ }
+ p = p.parent;
+ }
+ return false;
+ }
+
+ /**
+ * Return true if this node is a descendant of the parameter node.
+ *
+ * @param node a possible ancestor node
+ * @return true if this nodes descends from the given node
+ */
+ public boolean isDescendentOf(final PNode node) {
+ PNode p = parent;
+ while (p != null) {
+ if (p == node) {
+ return true;
+ }
+ p = p.parent;
+ }
+ return false;
+ }
+
+ /**
+ * Return true if this node descends from the root.
+ *
+ * @return whether this node descends from root node
+ */
+ public boolean isDescendentOfRoot() {
+ return getRoot() != null;
+ }
+
+ /**
+ * Change the order of this node in its parent's children list so that it
+ * will draw in back of all of its other sibling nodes.
+ */
+ public void moveToBack() {
+ final PNode p = parent;
+ if (p != null) {
+ p.removeChild(this);
+ p.addChild(0, this);
+ }
+ }
+
+ /**
+ * Change the order of this node in its parent's children list so that it
+ * will draw in back of the specified sibling node.
+ *
+ * @param sibling sibling to move in back of
+ */
+ public void moveInBackOf(final PNode sibling) {
+ final PNode p = parent;
+ if (p != null && p == sibling.getParent()) {
+ p.removeChild(this);
+ final int index = p.indexOfChild(sibling);
+ p.addChild(index, this);
+ }
+ }
+
+ /**
+ * Change the order of this node in its parent's children list so that it
+ * will draw in front of all of its other sibling nodes.
+ */
+ public void moveToFront() {
+ final PNode p = parent;
+ if (p != null) {
+ p.removeChild(this);
+ p.addChild(this);
+ }
+ }
+
+ /**
+ * Change the order of this node in its parent's children list so that it
+ * will draw in front of the specified sibling node.
+ *
+ * @param sibling sibling to move in front of
+ */
+ public void moveInFrontOf(final PNode sibling) {
+ final PNode p = parent;
+ if (p != null && p == sibling.getParent()) {
+ p.removeChild(this);
+ final int index = p.indexOfChild(sibling);
+ p.addChild(index + 1, this);
+ }
+ }
+
+ /**
+ * Return the parent of this node. This will be null if this node has not
+ * been added to a parent yet.
+ *
+ * @return this nodes parent or null
+ */
+ public PNode getParent() {
+ return parent;
+ }
+
+ /**
+ * Set the parent of this node. Note this is set automatically when adding
+ * and removing children.
+ *
+ * @param newParent the parent to which this node should be added
+ */
+ public void setParent(final PNode newParent) {
+ final PNode old = parent;
+ parent = newParent;
+ firePropertyChange(PROPERTY_CODE_PARENT, PROPERTY_PARENT, old, parent);
+ }
+
+ /**
+ * Return the index where the given child is stored.
+ *
+ * @param child child so search for
+ * @return index of child or -1 if not found
+ */
+ public int indexOfChild(final PNode child) {
+ if (children == null) {
+ return -1;
+ }
+ return children.indexOf(child);
+ }
+
+ /**
+ * Remove the given child from this node's children list. Any subsequent
+ * children are shifted to the left (one is subtracted from their indices).
+ * The removed child's parent is set to null.
+ *
+ * @param child the child to remove
+ * @return the removed child
+ */
+ public PNode removeChild(final PNode child) {
+ final int index = indexOfChild(child);
+ if (index == -1) {
+ return null;
+ }
+ return removeChild(index);
+ }
+
+ /**
+ * Remove the child at the specified position of this group node's children.
+ * Any subsequent children are shifted to the left (one is subtracted from
+ * their indices). The removed child's parent is set to null.
+ *
+ * @param index the index of the child to remove
+ * @return the removed child
+ */
+ public PNode removeChild(final int index) {
+ if (children == null) {
+ return null;
+ }
+ final PNode child = (PNode) children.remove(index);
+
+ if (children.size() == 0) {
+ children = null;
+ }
+
+ child.repaint();
+ child.setParent(null);
+ invalidateFullBounds();
+
+ firePropertyChange(PROPERTY_CODE_CHILDREN, PROPERTY_CHILDREN, null, children);
+
+ return child;
+ }
+
+ /**
+ * Remove all the children in the given collection from this node's list of
+ * children. All removed nodes will have their parent set to null.
+ *
+ * @param childrenNodes the collection of children to remove
+ */
+ public void removeChildren(final Collection childrenNodes) {
+ final Iterator i = childrenNodes.iterator();
+ while (i.hasNext()) {
+ final PNode each = (PNode) i.next();
+ removeChild(each);
+ }
+ }
+
+ /**
+ * Remove all the children from this node. Node this method is more
+ * efficient then removing each child individually.
+ */
+ public void removeAllChildren() {
+ if (children != null) {
+ final int count = children.size();
+ for (int i = 0; i < count; i++) {
+ final PNode each = (PNode) children.get(i);
+ each.setParent(null);
+ }
+ children = null;
+ invalidatePaint();
+ invalidateFullBounds();
+
+ firePropertyChange(PROPERTY_CODE_CHILDREN, PROPERTY_CHILDREN, null, children);
+ }
+ }
+
+ /**
+ * Delete this node by removing it from its parent's list of children.
+ */
+ public void removeFromParent() {
+ if (parent != null) {
+ parent.removeChild(this);
+ }
+ }
+
+ /**
+ * Set the parent of this node, and transform the node in such a way that it
+ * doesn't move in global coordinates.
+ *
+ * @param newParent The new parent of this node.
+ */
+ public void reparent(final PNode newParent) {
+ final AffineTransform originalTransform = getLocalToGlobalTransform(null);
+ final AffineTransform newTransform = newParent.getGlobalToLocalTransform(null);
+ newTransform.concatenate(originalTransform);
+
+ removeFromParent();
+ setTransform(newTransform);
+ newParent.addChild(this);
+ computeFullBounds(fullBoundsCache);
+ }
+
+ /**
+ * Swaps this node out of the scene graph tree, and replaces it with the
+ * specified replacement node. This node is left dangling, and it is up to
+ * the caller to manage it. The replacement node will be added to this
+ * node's parent in the same position as this was. That is, if this was the
+ * 3rd child of its parent, then after calling replaceWith(), the
+ * replacement node will also be the 3rd child of its parent. If this node
+ * has no parent when replace is called, then nothing will be done at all.
+ *
+ * @param replacementNode the new node that replaces the current node in the
+ * scene graph tree.
+ */
+ public void replaceWith(final PNode replacementNode) {
+ if (parent != null) {
+ final PNode p = parent;
+ final int index = p.getChildrenReference().indexOf(this);
+ p.removeChild(this);
+ p.addChild(index, replacementNode);
+ }
+ }
+
+ /**
+ * Sets the name of this null, may be null.
+ *
+ * @since 1.3
+ * @param name new name for this node
+ */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the name given to this node.
+ *
+ * @since 1.3
+ * @return name given to this node, may be null
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Return the number of children that this node has.
+ *
+ * @return the number of children
+ */
+ public int getChildrenCount() {
+ if (children == null) {
+ return 0;
+ }
+ return children.size();
+ }
+
+ /**
+ * Return the child node at the specified index.
+ *
+ * @param index a child index
+ * @return the child node at the specified index
+ */
+ public PNode getChild(final int index) {
+ return (PNode) children.get(index);
+ }
+
+ /**
+ * Return a reference to the list used to manage this node's children. This
+ * list should not be modified.
+ *
+ * @return reference to the children list
+ */
+ public List getChildrenReference() {
+ if (children == null) {
+ children = new ArrayList();
+ }
+ return children;
+ }
+
+ /**
+ * Return an iterator over this node's direct descendant children.
+ *
+ * @return iterator over this nodes children
+ */
+ public ListIterator getChildrenIterator() {
+ if (children == null) {
+ return Collections.EMPTY_LIST.listIterator();
+ }
+ return Collections.unmodifiableList(children).listIterator();
+ }
+
+ /**
+ * Return the root node (instance of PRoot). If this node does not descend
+ * from a PRoot then null will be returned.
+ *
+ * @return root element of this node, or null if this node does not descend
+ * from a PRoot
+ */
+ public PRoot getRoot() {
+ if (parent != null) {
+ return parent.getRoot();
+ }
+ return null;
+ }
+
+ /**
+ * Return a collection containing this node and all of its descendant nodes.
+ *
+ * @return a new collection containing this node and all descendants
+ */
+ public Collection getAllNodes() {
+ return getAllNodes(null, null);
+ }
+
+ /**
+ * Return a collection containing the subset of this node and all of its
+ * descendant nodes that are accepted by the given node filter. If the
+ * filter is null then all nodes will be accepted. If the results parameter
+ * is not null then it will be used to collect this subset instead of
+ * creating a new collection.
+ *
+ * @param filter the filter used to determine the subset
+ * @param resultantNodes where matching nodes should be added
+ * @return a collection containing this node and all descendants
+ */
+ public Collection getAllNodes(final PNodeFilter filter, final Collection resultantNodes) {
+ Collection results;
+ if (resultantNodes == null) {
+ results = new ArrayList();
+ }
+ else {
+ results = resultantNodes;
+ }
+
+ if (filter == null || filter.accept(this)) {
+ results.add(this);
+ }
+
+ if (filter == null || filter.acceptChildrenOf(this)) {
+ final int count = getChildrenCount();
+ for (int i = 0; i < count; i++) {
+ final PNode each = (PNode) children.get(i);
+ each.getAllNodes(filter, results);
+ }
+ }
+
+ return results;
+ }
+
+ // ****************************************************************
+ // Serialization - Nodes conditionally serialize their parent.
+ // This means that only the parents that were unconditionally
+ // (using writeObject) serialized by someone else will be restored
+ // when the node is unserialized.
+ // ****************************************************************
+
+ /**
+ * Write this node and all of its descendant nodes to the given outputsteam.
+ * This stream must be an instance of PObjectOutputStream or serialization
+ * will fail. This nodes parent is written out conditionally, that is it
+ * will only be written out if someone else writes it out unconditionally.
+ *
+ * @param out the output stream to write to, must be an instance of
+ * PObjectOutputStream
+ * @throws IOException when an error occurs speaking to underlying
+ * ObjectOutputStream
+ */
+ private void writeObject(final ObjectOutputStream out) throws IOException {
+ if (!(out instanceof PObjectOutputStream)) {
+ throw new IllegalArgumentException("PNode.writeObject may only be used with PObjectOutputStreams");
+ }
+ out.defaultWriteObject();
+ ((PObjectOutputStream) out).writeConditionalObject(parent);
+ }
+
+ /**
+ * Read this node and all of its descendants in from the given input stream.
+ *
+ * @param in the stream to read from
+ *
+ * @throws IOException when an error occurs speaking to underlying
+ * ObjectOutputStream
+ * @throws ClassNotFoundException when a class is deserialized that no
+ * longer exists. This can happen if it's renamed or deleted.
+ */
+ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ parent = (PNode) in.readObject();
+ }
+
+ /**
+ * @deprecated see http://code.google.com/p/piccolo2d/issues/detail?id=99
+ *
+ * @return a string representation of this node's state
+ */
+ protected String paramString() {
+ return "";
+ }
+
+ /**
+ * Returns an array of input event listeners that are attached to this node.
+ *
+ * @since 1.3
+ * @return event listeners attached to this node
+ */
+ public PInputEventListener[] getInputEventListeners() {
+ if (listenerList == null || listenerList.getListenerCount() == 0) {
+ return new PInputEventListener[] {};
+ }
+
+ final EventListener[] listeners = listenerList.getListeners(PInputEventListener.class);
+
+ final PInputEventListener[] result = new PInputEventListener[listeners.length];
+ for (int i = 0; i < listeners.length; i++) {
+ result[i] = (PInputEventListener) listeners[i];
+ }
+ return result;
+ }
+
+ private static final class ClientPropertyKeyIterator implements Iterator {
+ private final Enumeration enumeration;
+
+ private ClientPropertyKeyIterator(final Enumeration enumeration) {
+ this.enumeration = enumeration;
+ }
+
+ public boolean hasNext() {
+ return enumeration.hasMoreElements();
+ }
+
+ public Object next() {
+ return enumeration.nextElement();
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * PSceneGraphDelegate is an interface to receive low level node
+ * events. It together with PNode.SCENE_GRAPH_DELEGATE gives Piccolo2d users
+ * an efficient way to learn about low level changes in Piccolo's scene
+ * graph. Most users will not need to use this.
+ */
+ public interface PSceneGraphDelegate {
+ /**
+ * Called to notify delegate that the node needs repainting.
+ *
+ * @param node node needing repaint
+ */
+ void nodePaintInvalidated(PNode node);
+
+ /**
+ * Called to notify delegate that the node and all it's children need
+ * repainting.
+ *
+ * @param node node needing repaint
+ */
+ void nodeFullBoundsInvalidated(PNode node);
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolo/POffscreenCanvas.java b/src/main/java/edu/umd/cs/piccolo/POffscreenCanvas.java
new file mode 100644
index 0000000..8d160e8
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/POffscreenCanvas.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo;
+
+import java.awt.Cursor;
+import java.awt.Graphics2D;
+
+import edu.umd.cs.piccolo.util.PBounds;
+import edu.umd.cs.piccolo.util.PPaintContext;
+import edu.umd.cs.piccolo.util.PUtil;
+
+/**
+ * Offscreen canvas.
+ *
+ * @since 1.3
+ */
+public final class POffscreenCanvas implements PComponent {
+
+ /** Bounds of this offscreen canvas. */
+ private final PBounds bounds;
+
+ /** Camera for this offscreen canvas. */
+ private PCamera camera;
+
+ /** Render quality. */
+ private int renderQuality = DEFAULT_RENDER_QUALITY;
+
+ /** Default render quality, PPaintContext.HIGH_QUALITY_RENDERING
. */
+ static final int DEFAULT_RENDER_QUALITY = PPaintContext.HIGH_QUALITY_RENDERING;
+
+
+ /**
+ * Create a new offscreen canvas the specified width and height.
+ *
+ * @param width width of this offscreen canvas, must be at least zero
+ * @param height height of this offscreen canvas, must be at least zero
+ */
+ public POffscreenCanvas(final int width, final int height) {
+ if (width < 0) {
+ throw new IllegalArgumentException("width must be at least zero, was " + width);
+ }
+ if (height < 0) {
+ throw new IllegalArgumentException("height must be at least zero, was " + height);
+ }
+ bounds = new PBounds(0.0d, 0.0d, width, height);
+ setCamera(PUtil.createBasicScenegraph());
+ }
+
+ /**
+ * Render this offscreen canvas to the specified graphics.
+ *
+ * @param graphics graphics to render this offscreen canvas to, must not be
+ * null
+ */
+ public void render(final Graphics2D graphics) {
+ if (graphics == null) {
+ throw new IllegalArgumentException("graphics must not be null");
+ }
+ final PPaintContext paintContext = new PPaintContext(graphics);
+ paintContext.setRenderQuality(renderQuality);
+ camera.fullPaint(paintContext);
+ }
+
+ /**
+ * Set the camera for this offscreen canvas to camera
.
+ *
+ * @param camera camera for this offscreen canvas
+ */
+ public void setCamera(final PCamera camera) {
+ if (this.camera != null) {
+ this.camera.setComponent(null);
+ }
+ this.camera = camera;
+ if (camera != null) {
+ camera.setComponent(this);
+ camera.setBounds((PBounds) bounds.clone());
+ }
+ }
+
+ /**
+ * Return the camera for this offscreen canvas.
+ *
+ * @return the camera for this offscreen canvas
+ */
+ public PCamera getCamera() {
+ return camera;
+ }
+
+ /**
+ * Set the render quality hint for this offscreen canvas to
+ * renderQuality
.
+ *
+ * @param renderQuality render quality hint, must be one of
+ * PPaintContext.HIGH_QUALITY_RENDERING
or
+ * PPaintContext.LOW_QUALITY_RENDERING
+ */
+ public void setRenderQuality(final int renderQuality) {
+ if (renderQuality == PPaintContext.HIGH_QUALITY_RENDERING
+ || renderQuality == PPaintContext.LOW_QUALITY_RENDERING) {
+ this.renderQuality = renderQuality;
+ }
+ else {
+ throw new IllegalArgumentException("renderQuality must be one of PPaintContext.HIGH_QUALITY_RENDERING"
+ + " or PPaintContext.LOW_QUALITY_RENDERING, was " + renderQuality);
+ }
+ }
+
+ /**
+ * Return the render quality hint for this offscreen canvas.
+ *
+ * @return the render quality hint for this offscreen canvas
+ */
+ public int getRenderQuality() {
+ return renderQuality;
+ }
+
+ /** {@inheritDoc} */
+ public void paintImmediately() {
+ // empty
+ }
+
+ /** {@inheritDoc} */
+ public void popCursor() {
+ // empty
+ }
+
+ /** {@inheritDoc} */
+ public void pushCursor(final Cursor cursor) {
+ // empty
+ }
+
+ /** {@inheritDoc} */
+ public void repaint(final PBounds repaintBounds) {
+ // empty
+ }
+
+ /** {@inheritDoc} */
+ public void setInteracting(final boolean interacting) {
+ // empty
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolo/PRoot.java b/src/main/java/edu/umd/cs/piccolo/PRoot.java
new file mode 100644
index 0000000..b87c9c1
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/PRoot.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo;
+
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+import edu.umd.cs.piccolo.activities.PActivity;
+import edu.umd.cs.piccolo.activities.PActivityScheduler;
+import edu.umd.cs.piccolo.util.PDebug;
+import edu.umd.cs.piccolo.util.PNodeFilter;
+
+/**
+ * PRoot serves as the top node in Piccolo2D's runtime structure. The
+ * PRoot responsible for running the main UI loop that processes input from
+ * activities and external events.
+ *
+ *
+ * @version 1.1
+ * @author Jesse Grosjean
+ */
+public class PRoot extends PNode {
+
+ /**
+ * Allows for future serialization code to understand versioned binary
+ * formats.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The property name that identifies a change in the set of this root's
+ * input sources (see {@link InputSource InputSource}). In any property
+ * change event the new value will be a reference to the list of this root's
+ * input sources, but old value will always be null.
+ */
+ public static final String PROPERTY_INPUT_SOURCES = "inputSources";
+
+ /**
+ * The property code that identifies a change in the set of this root's
+ * input sources (see {@link InputSource InputSource}). In any property
+ * change event the new value will be a reference to the list of this root's
+ * input sources, but old value will always be null.
+ */
+ public static final int PROPERTY_CODE_INPUT_SOURCES = 1 << 14;
+
+ /**
+ * The property name that identifies a change in this node's interacting
+ * state.
+ *
+ * @since 1.3
+ */
+ public static final String PROPERTY_INTERACTING_CHANGED = "INTERACTING_CHANGED_NOTIFICATION";
+
+ /**
+ * The property code that identifies a change in this node's interacting
+ * state.
+ *
+ * @since 1.3
+ */
+ public static final int PROPERTY_CODE_INTERACTING_CHANGED = 1 << 13;
+
+ /** Whether this not is currently processing inputs. */
+ protected transient boolean processingInputs;
+
+ /** Whether this node needs to have its inputs processed. */
+ protected transient boolean processInputsScheduled;
+
+ /** The number of interactions this node is currently participating in. */
+ private transient int interacting;
+
+ /**
+ * The singleton instance of the default input manager.
+ */
+ private transient PInputManager defaultInputManager;
+
+ /** The Input Sources that are registered to this node. */
+ private final transient List inputSources;
+
+ /**
+ * Used to provide a consistent clock time to activities as they are being
+ * processed.
+ *
+ * Should it happen that an activity step take longer than a millisecond,
+ * the next step will be unaffected by the change in clock had it used
+ * System.currentMillis().
+ */
+ private transient long globalTime;
+
+ /**
+ * Object responsible for scheduling activities, regardless of where in the
+ * scene they take place.
+ */
+ private final PActivityScheduler activityScheduler;
+
+ /**
+ * Construct a new PRoot(). Note the PCanvas already creates a basic scene
+ * graph for you so often you will not need to construct your own roots.
+ */
+ public PRoot() {
+ super();
+ inputSources = new ArrayList();
+ globalTime = System.currentTimeMillis();
+ activityScheduler = new PActivityScheduler(this);
+ }
+
+ // ****************************************************************
+ // Activities
+ // ****************************************************************
+
+ /**
+ * Add an activity to the activity scheduler associated with this root.
+ * Activities are given a chance to run during each call to the roots
+ * processInputs
method. When the activity has finished running
+ * it will automatically get removed.
+ *
+ * @param activity Activity that should be scheduled
+ * @return whether it has been scheduled (always true)
+ */
+ public boolean addActivity(final PActivity activity) {
+ getActivityScheduler().addActivity(activity);
+ return true;
+ }
+
+ /**
+ * Get the activity scheduler associated with this root.
+ *
+ * @return associated scheduler
+ */
+ public PActivityScheduler getActivityScheduler() {
+ return activityScheduler;
+ }
+
+ /**
+ * Wait for all scheduled activities to finish before returning from this
+ * method. This will freeze out user input, and so it is generally
+ * recommended that you use PActivities.setTriggerTime() to offset
+ * activities instead of using this method.
+ */
+ public void waitForActivities() {
+ final PNodeFilter cameraWithCanvas = new CameraWithCanvasFilter();
+
+ while (activityScheduler.getActivitiesReference().size() > 0) {
+ processInputs();
+ final Iterator i = getAllNodes(cameraWithCanvas, null).iterator();
+ while (i.hasNext()) {
+ final PCamera each = (PCamera) i.next();
+ each.getComponent().paintImmediately();
+ }
+ }
+ }
+
+ /**
+ * Since getRoot is handled recursively, and root is the lowest point in the
+ * hierarchy, simply returns itself.
+ *
+ * @return itself
+ */
+ public PRoot getRoot() {
+ return this;
+ }
+
+ /**
+ * Get the default input manager to be used when processing input events.
+ * PCanvas's use this method when they forward new swing input events to the
+ * PInputManager.
+ *
+ * @return a singleton instance of PInputManager
+ */
+ public PInputManager getDefaultInputManager() {
+ if (defaultInputManager == null) {
+ defaultInputManager = new PInputManager();
+ addInputSource(defaultInputManager);
+ }
+ return defaultInputManager;
+ }
+
+ /**
+ * Return true if this root has been marked as interacting. If so the root
+ * will normally render at a lower quality that is faster.
+ *
+ * @since 1.3
+ * @return true if this root has user interaction taking place
+ */
+ public boolean getInteracting() {
+ return interacting > 0;
+ }
+
+ /**
+ * Set if this root is interacting. If so the root will normally render at a
+ * lower quality that is faster. Also repaints the root if the the
+ * interaction has ended.
+ *
processInputs
method.
+ * Activities should usually use this global time instead of System.
+ * currentTimeMillis() so that multiple activities will be synchronized.
+ *
+ * @return time as recorded at the beginning of activity scheduling
+ */
+ public long getGlobalTime() {
+ return globalTime;
+ }
+
+ /**
+ * This is the heartbeat of the Piccolo2D framework. Pending input events
+ * are processed. Activities are given a chance to run, and the bounds
+ * caches and any paint damage is validated.
+ */
+ public void processInputs() {
+ PDebug.startProcessingInput();
+ processingInputs = true;
+
+ globalTime = System.currentTimeMillis();
+ if (inputSources.size() > 0) {
+ final Iterator inputSourceIterator = inputSources.iterator();
+ while (inputSourceIterator.hasNext()) {
+ final InputSource each = (InputSource) inputSourceIterator.next();
+ each.processInput();
+ }
+ }
+
+ activityScheduler.processActivities(globalTime);
+ validateFullBounds();
+ validateFullPaint();
+
+ processingInputs = false;
+ PDebug.endProcessingInput();
+ }
+
+ /** {@inheritDoc} */
+ public void setFullBoundsInvalid(final boolean fullLayoutInvalid) {
+ super.setFullBoundsInvalid(fullLayoutInvalid);
+ scheduleProcessInputsIfNeeded();
+ }
+
+ /** {@inheritDoc} */
+ public void setChildBoundsInvalid(final boolean childLayoutInvalid) {
+ super.setChildBoundsInvalid(childLayoutInvalid);
+ scheduleProcessInputsIfNeeded();
+ }
+
+ /** {@inheritDoc} */
+ public void setPaintInvalid(final boolean paintInvalid) {
+ super.setPaintInvalid(paintInvalid);
+ scheduleProcessInputsIfNeeded();
+ }
+
+ /** {@inheritDoc} */
+ public void setChildPaintInvalid(final boolean childPaintInvalid) {
+ super.setChildPaintInvalid(childPaintInvalid);
+ scheduleProcessInputsIfNeeded();
+ }
+
+ /**
+ * Schedule process inputs if needed.
+ */
+ public void scheduleProcessInputsIfNeeded() {
+ /*
+ * The reason for the special case here (when not in the event dispatch
+ * thread) is that the SwingUtilitiles.invokeLater code below only
+ * invokes later with respect to the event dispatch thread, it will
+ * invoke concurrently with other threads.
+ */
+ if (!SwingUtilities.isEventDispatchThread()) {
+ /*
+ * Piccolo2D is not thread safe and should almost always be called
+ * from the Swing event dispatch thread. It should only reach this
+ * point when a new canvas is being created.
+ */
+ return;
+ }
+
+ PDebug.scheduleProcessInputs();
+
+ if (!processInputsScheduled && !processingInputs
+ && (getFullBoundsInvalid() || getChildBoundsInvalid() || getPaintInvalid() || getChildPaintInvalid())) {
+
+ processInputsScheduled = true;
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ processInputs();
+ processInputsScheduled = false;
+ }
+ });
+ }
+ }
+
+ private static final class CameraWithCanvasFilter implements PNodeFilter {
+ public boolean accept(final PNode aNode) {
+ return aNode instanceof PCamera && ((PCamera) aNode).getComponent() != null;
+ }
+
+ public boolean acceptChildrenOf(final PNode aNode) {
+ return true;
+ }
+ }
+
+ /**
+ * This interfaces is for advanced use only. If you want to implement a
+ * different kind of input framework then Piccolo2D provides you can hook it
+ * in here.
+ */
+ public static interface InputSource {
+ /** Causes the system to process any pending Input Events. */
+ void processInput();
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolo/activities/PActivity.java b/src/main/java/edu/umd/cs/piccolo/activities/PActivity.java
new file mode 100644
index 0000000..0718177
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/activities/PActivity.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo.activities;
+
+import edu.umd.cs.piccolo.util.PUtil;
+
+/**
+ * PActivity controls some time dependent aspect of Piccolo, such as
+ * animation. Once created activities must be scheduled with the
+ * PActivityScheduler managed by the PRoot to run. They are automatically
+ * removed from the scheduler when the animation has finished.
+ * + * See the PNode.animate*() methods for an example of how to set up and run + * different activities. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PActivity { + /** + * Parameter for terminate that signifies that activity should bail out + * immediately without flagging activity as finished. + */ + public static final int TERMINATE_WITHOUT_FINISHING = 0; + + /** + * Parameter for terminate that signifies that activity should bail out + * immediately, but flag activity as finished. + */ + public static final int TERMINATE_AND_FINISH = 1; + + /** + * Parameter for terminate that signifies that activity should bail out + * immediately, if currently active. + */ + public static final int TERMINATE_AND_FINISH_IF_STEPPING = 2; + + /** Activity scheduler that this activity is bound to. */ + private PActivityScheduler scheduler; + + /** Time at which this activity should start in PRoot global time. */ + private long startTime; + + /** Duration in milliseconds that this activity should last. */ + private long duration; + + /** How many milliseconds should pass between steps. */ + private long stepRate; + + private PActivityDelegate delegate; + + /** Whether this activity is currently active. */ + private boolean stepping; + + /** Next time at which step should occur. */ + private long nextStepTime; + + /** + * Constructs a new PActivity. + * + * @param aDuration the amount of time that this activity should take to + * complete, -1 for infinite. + */ + public PActivity(final long aDuration) { + this(aDuration, PUtil.DEFAULT_ACTIVITY_STEP_RATE); + } + + /** + * Constructs a new PActivity. + * + * @param aDuration the amount of time that this activity should take to + * complete, -1 for infinite. + * @param aStepRate the maximum rate that this activity should receive step + * events. + */ + public PActivity(final long aDuration, final long aStepRate) { + this(aDuration, aStepRate, System.currentTimeMillis()); + } + + /** + * Constructs a new PActivity. + * + * @param aDuration the amount of time that this activity should take to + * complete, -1 for infinite. + * @param aStepRate the maximum rate that this activity should receive step + * events. + * @param aStartTime the time (relative to System.currentTimeMillis()) that + * this activity should start. + */ + public PActivity(final long aDuration, final long aStepRate, final long aStartTime) { + duration = aDuration; + stepRate = aStepRate; + startTime = aStartTime; + nextStepTime = aStartTime; + stepping = false; + } + + // **************************************************************** + // Basics + // **************************************************************** + + /** + * Return the time that this activity should start running in PRoot global + * time. When this time is reached (or soon after) this activity will have + * its startStepping() method called. + * + * @return time at which this activity should start in PRoot global time. + */ + public long getStartTime() { + return startTime; + } + + /** + * Set the time that this activity should start running in PRoot global + * time. When this time is reached (or soon after) this activity will have + * its startStepping() method called. + * + * @param aTriggerTime time at which you want this activity to begin in + * PRoot global time + */ + public void setStartTime(final long aTriggerTime) { + startTime = aTriggerTime; + } + + /** + * Return the amount of time that this activity should delay between steps. + * + * @return the desired milliseconds between steps + */ + public long getStepRate() { + return stepRate; + } + + /** + * Set the amount of time that this activity should delay between steps. + * + * @param aStepRate desired step rate in milliseconds between steps + */ + public void setStepRate(final long aStepRate) { + stepRate = aStepRate; + } + + /** + * Gets the next step time desired for this activity. Exists since some + * steps might eat into the step rate otherwise. + * + * @return next calculated step time + */ + public long getNextStepTime() { + return nextStepTime; + } + + /** + * Return the amount of time that this activity should take to complete, + * after the startStepping method is called. + * + * @return time that this activity should take to complete + */ + public long getDuration() { + return duration; + } + + /** + * Set the amount of time that this activity should take to complete, after + * the startStepping method is called. + * + * @param aDuration desired duration this activity should take (-1 for + * infinite) once it begins stepping + */ + public void setDuration(final long aDuration) { + duration = aDuration; + } + + /** + * Returns the activity scheduler associated with this activity. + * + * @return associated scheduler + */ + public PActivityScheduler getActivityScheduler() { + return scheduler; + } + + /** + * Informs the activity of the scheduler that will be responsible for + * scheduling it. + * + * @param aScheduler scheduler to associate with this activity + */ + public void setActivityScheduler(final PActivityScheduler aScheduler) { + scheduler = aScheduler; + } + + // **************************************************************** + // Stepping + // **************************************************************** + + /** + * Return true if this activity is stepping. + * + * @return whether this activity is stepping + */ + public boolean isStepping() { + return stepping; + } + + /** + * Return true if this activity is performing an animation. This is used by + * the PCanvas to determine if it should set the render quality to + * PCanvas.animatingRenderQuality or not for each frame it renders. + * + * @return whether this activity is an animation, subclasses can override + * this. + */ + protected boolean isAnimation() { + return false; + } + + /** + * This method is called right before an activity is scheduled to start + * running. After this method is called step() will be called until the + * activity finishes. + */ + protected void activityStarted() { + if (delegate != null) { + delegate.activityStarted(this); + } + } + + /** + * This is the method that most activities override to perform their + * behavior. It will be called repeatedly when the activity is running. + * + * @param elapsedTime the amount of time that has passed relative to the + * activities startTime. + */ + protected void activityStep(final long elapsedTime) { + if (delegate != null) { + delegate.activityStepped(this); + } + } + + /** + * This method is called after an activity is has finished running and the + * activity has been removed from the PActivityScheduler queue. + */ + protected void activityFinished() { + if (delegate != null) { + delegate.activityFinished(this); + } + } + + /** + * Get the delegate for this activity. The delegate is notified when the + * activity starts and stops stepping. + * + * @return delegate of this activity, may be null + */ + public PActivityDelegate getDelegate() { + return delegate; + } + + /** + * Set the delegate for this activity. The delegate is notified when the + * activity starts and stops stepping. + * + * @param delegate delegate that should be informed of activity events + */ + public void setDelegate(final PActivityDelegate delegate) { + this.delegate = delegate; + } + + // **************************************************************** + // Controlling + // **************************************************************** + + /** + * Schedules this activity to start after the first activity has finished. + * Note that no link is created between these activities, if the startTime + * or duration of the first activity is later changed this activities start + * time will not be updated to reflect that change. + * + * @param first activity after which this activity should be scheduled + */ + public void startAfter(final PActivity first) { + setStartTime(first.getStartTime() + first.getDuration()); + } + + /** + * Stop this activity immediately, and remove it from the activity + * scheduler. The default termination behavior is call activityFinished if + * the activity is currently stepping. Use terminate(terminationBehavior) + * use a different termination behavior. + */ + public void terminate() { + terminate(TERMINATE_AND_FINISH_IF_STEPPING); + } + + /** + * Stop this activity immediately, and remove it from the activity + * scheduler. The termination behavior determines when and if + * activityStarted and activityFinished get called. The possible termination + * behaviors are as follow: + * + * TERMINATE_WITHOUT_FINISHING - The method activityFinished will never get + * called and so the activity will be terminated midway. + * TERMINATE_AND_FINISH - The method activityFinished will always get + * called. And so the activity will always end in it's completed state. If + * the activity has not yet started the method activityStarted will also be + * called. TERMINATE_AND_FINISH_IF_STEPPING - The method activityFinished + * will only be called if the activity has previously started. + * + * @param terminationBehavior behavior to use regarding delegate + * notification and event firing + */ + public void terminate(final int terminationBehavior) { + if (scheduler != null) { + scheduler.removeActivity(this); + } + + switch (terminationBehavior) { + case TERMINATE_WITHOUT_FINISHING: + stepping = false; + break; + + case TERMINATE_AND_FINISH: + if (stepping) { + stepping = false; + activityFinished(); + } + else { + activityStarted(); + activityFinished(); + } + + break; + + case TERMINATE_AND_FINISH_IF_STEPPING: + if (stepping) { + stepping = false; + activityFinished(); + } + break; + default: + throw new RuntimeException("Invalid termination behaviour provided to PActivity.terminate"); + } + } + + /** + * The activity scheduler calls this method and it is here that the activity + * decides if it should do a step or not for the given time. + * + * @param currentTime in global root time + * @return number of milliseconds in global root time before processStep + * should be called again, -1 if never + */ + public long processStep(final long currentTime) { + // if before start time + if (currentTime < startTime) { + return startTime - currentTime; + } + + // if past stop time + if (currentTime > getStopTime()) { + if (stepping) { + stepping = false; + scheduler.removeActivity(this); + activityFinished(); + } + else { + activityStarted(); + scheduler.removeActivity(this); + activityFinished(); + } + return -1; + } + + // else should be stepping + if (!stepping) { + activityStarted(); + stepping = true; + } + + if (currentTime >= nextStepTime) { + activityStep(currentTime - startTime); + nextStepTime = currentTime + stepRate; + } + + return stepRate; + } + + /** + * Return the time when this activity should finish running. At this time + * (or soon after) the stoppedStepping method will be called + * + * @return time at which this activity should be stopped + */ + public long getStopTime() { + if (duration == -1) { + return Long.MAX_VALUE; + } + return startTime + duration; + } + + /** + * @deprecated see http://code.google.com/p/piccolo2d/issues/detail?id=99 + * + * @return string representation of this activity + */ + protected String paramString() { + return ""; + } + + /** + * PActivityDelegate is used by classes to learn about and act on the + * different states that a PActivity goes through, such as when the activity + * starts and stops stepping. + */ + public interface PActivityDelegate { + /** + * Gets called when the activity starts. + * + * @param activity activity that started + */ + void activityStarted(PActivity activity); + + /** + * Gets called for each step of the activity. + * + * @param activity activity that is stepping + */ + void activityStepped(PActivity activity); + + /** + * Gets called when the activity finishes. + * + * @param activity activity that finished + */ + void activityFinished(PActivity activity); + } + +} diff --git a/src/main/java/edu/umd/cs/piccolo/activities/PActivityScheduler.java b/src/main/java/edu/umd/cs/piccolo/activities/PActivityScheduler.java new file mode 100644 index 0000000..9b93951 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/activities/PActivityScheduler.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.activities; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.Timer; + +import edu.umd.cs.piccolo.PRoot; +import edu.umd.cs.piccolo.util.PUtil; + +/** + * PActivityScheduler is responsible for maintaining a list of + * activities. It is given a chance to process these activities from the PRoot's + * processInputs() method. Most users will not need to use the + * PActivityScheduler directly, instead you should look at: + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PColorActivity extends PInterpolatingActivity { + + private Color source; + private Color destination; + private final Target target; + + /** + * Target Objects that want their color to be set by the color + * activity must implement this interface. + */ + public interface Target { + + /** + * This will be called by the color activity for each new interpolated + * color that it computes while it is stepping. + * + * @param color the color to assign to the target + */ + void setColor(Color color); + + /** + * This method is called right before the color activity starts. That + * way an object's color is always animated from its current color. + * + * @return the target's current color. + */ + Color getColor(); + } + + /** + * Constructs a color activity for the given target that will animate for + * the duration provided at an interval of stepRate. + * + * Destination color must be assigned later. + * + * @param duration duration in milliseconds that the animation should last + * @param stepRate the time between interpolations + * @param aTarget the target onto which the animation is being performed + */ + public PColorActivity(final long duration, final long stepRate, final Target aTarget) { + this(duration, stepRate, aTarget, null); + } + + /** + * Constructs a color activity for the given target that will animate for + * the duration provided at an interval of stepRate from the target's + * starting color to the destination color. + * + * @param duration duration in milliseconds that the animation should last + * @param stepRate the time between interpolations + * @param aTarget the target onto which the animation is being performed + * @param aDestination the color to which the animation is aiming at + */ + public PColorActivity(final long duration, final long stepRate, final Target aTarget, final Color aDestination) { + this(duration, stepRate, 1, PInterpolatingActivity.SOURCE_TO_DESTINATION, aTarget, aDestination); + } + + /** + * Create a new PColorActivity. + * + * @param duration the length of one loop of the activity + * @param stepRate the amount of time between steps of the activity + * @param loopCount number of times the activity should reschedule itself + * @param mode defines how the activity interpolates between states + * @param aTarget the object that the activity will be applied to and where + * the source state will be taken from. + * @param aDestination the destination color state + */ + public PColorActivity(final long duration, final long stepRate, final int loopCount, final int mode, + final Target aTarget, final Color aDestination) { + super(duration, stepRate, loopCount, mode); + target = aTarget; + destination = aDestination; + } + + /** + * Returns true since all PColorActivities animate the scene. + * + * @return always returns true + */ + protected boolean isAnimation() { + return true; + } + + /** + * Return the final color that will be set on the color activities target + * when the activity stops stepping. + * + * @return the final color for this color activity + */ + public Color getDestinationColor() { + return destination; + } + + /** + * Set the final color that will be set on the color activities target when + * the activity stops stepping. + * + * @param newDestination to animate towards + */ + public void setDestinationColor(final Color newDestination) { + destination = newDestination; + } + + /** + * Overrides it's parent to ensure that the source color is the color of the + * node being animated. + */ + protected void activityStarted() { + if (getFirstLoop()) { + source = target.getColor(); + if (target == null) System.out.println("target is null2"); + if (source == null) System.out.println("source is null2"); + } + super.activityStarted(); + } + + /** + * Interpolates the target node's color by mixing the source color and the + * destination color. + * + * @param zeroToOne 0 = all source color, 1 = all destination color + */ + public void setRelativeTargetValue(final float zeroToOne) { + super.setRelativeTargetValue(zeroToOne); + if (target == null) System.out.println("target is null"); + if (source == null) System.out.println("source is null"); + if (destination == null) System.out.println("dest is null"); + final float red = source.getRed() + zeroToOne * (destination.getRed() - source.getRed()); + final float green = source.getGreen() + zeroToOne * (destination.getGreen() - source.getGreen()); + final float blue = source.getBlue() + zeroToOne * (destination.getBlue() - source.getBlue()); + final float alpha = source.getAlpha() + zeroToOne * (destination.getAlpha() - source.getAlpha()); + target.setColor(new Color((int) red, (int) green, (int) blue, (int) alpha)); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/activities/PInterpolatingActivity.java b/src/main/java/edu/umd/cs/piccolo/activities/PInterpolatingActivity.java new file mode 100644 index 0000000..c9001eb --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/activities/PInterpolatingActivity.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.activities; + +import edu.umd.cs.piccolo.util.PUtil; + +/** + * PInterpolatingActivity interpolates between two states (source and + * destination) over the duration of the activity. The interpolation can be + * either linear or slow- in, slow-out. + *
+ * The mode determines how the activity interpolates between the two states. The + * default mode interpolates from source to destination, but you can also go + * from destination to source, and from source to destination to source. + *
+ * A loopCount of greater then one will make the activity reschedule itself when + * it has finished. This makes the activity loop between the two states. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PInterpolatingActivity extends PActivity { + + /** + * Specifies that interpolation will be from the source value to the + * destination value. + */ + public static final int SOURCE_TO_DESTINATION = 1; + + /** + * Specifies that interpolation will be from the destination value to the + * source value. + */ + public static final int DESTINATION_TO_SOURCE = 2; + + /** + * Specifies that interpolation proceed from the source to the destination + * then back to the source. Can be used to perform flashes. source value. + */ + public static final int SOURCE_TO_DESTINATION_TO_SOURCE = 3; + + private int mode; + private boolean slowInSlowOut; + private int loopCount; + private boolean firstLoop; + + /** + * Constructs an interpolating activity that will last the duration given. + * + * @since 1.3 + * @param duration duration in milliseconds of the entire activity + */ + public PInterpolatingActivity(final long duration) { + this(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, 1, PInterpolatingActivity.SOURCE_TO_DESTINATION); + } + + /** + * Constructs an interpolating activity that will last the duration given + * and will update its target at the given rate. + * + * @param duration duration in milliseconds of the entire activity + * @param stepRate interval in milliseconds between updates to target + */ + public PInterpolatingActivity(final long duration, final long stepRate) { + this(duration, stepRate, 1, PInterpolatingActivity.SOURCE_TO_DESTINATION); + } + + /** + * Constructs an interpolating activity that will last the duration given + * and will update its target at the given rate. Once done, it will repeat + * the loopCount times. + * + * @param duration duration in milliseconds of the entire activity + * @param stepRate interval in milliseconds between updates to target + * @param loopCount # of times to repeat this activity. + * @param mode controls the direction of the interpolation (source to + * destination, destination to source, or source to destination + * back to source) + */ + public PInterpolatingActivity(final long duration, final long stepRate, final int loopCount, final int mode) { + this(duration, stepRate, System.currentTimeMillis(), loopCount, mode); + } + + /** + * Create a new PInterpolatingActivity. + *
+ * + * @param duration the length of one loop of the activity + * @param stepRate the amount of time between steps of the activity + * @param startTime the time (relative to System.currentTimeMillis()) that + * this activity should start. This value can be in the future. + * @param loopCount number of times the activity should reschedule itself + * @param mode defines how the activity interpolates between states + */ + public PInterpolatingActivity(final long duration, final long stepRate, final long startTime, final int loopCount, + final int mode) { + super(duration, stepRate, startTime); + this.loopCount = loopCount; + this.mode = mode; + slowInSlowOut = true; + firstLoop = true; + } + + /** + * Set the amount of time that this activity should take to complete, after + * the startStepping method is called. The duration must be greater then + * zero so that the interpolation value can be computed. + * + * @param duration new duration of this activity + */ + public void setDuration(final long duration) { + if (duration <= 0) { + throw new IllegalArgumentException("Duration for PInterpolatingActivity must be greater then 0"); + } + + super.setDuration(duration); + } + + // **************************************************************** + // Basics. + // **************************************************************** + + /** + * Return the mode used for interpolation. + * + * Acceptable values are: SOURCE_TO_DESTINATION, DESTINATION_TO_SOURCE and + * SOURCE_TO_DESTINATION_TO_SOURCE + * + * @return current mode of this activity + */ + public int getMode() { + return mode; + } + + /** + * Set the direction in which interpolation is going to occur. + * + * Acceptable values are: SOURCE_TO_DESTINATION, DESTINATION_TO_SOURCE and + * SOURCE_TO_DESTINATION_TO_SOURCE + * + * @param mode the new mode to use when interpolating + */ + public void setMode(final int mode) { + this.mode = mode; + } + + /** + * Return the number of times the activity should automatically reschedule + * itself after it has finished. + * + * @return number of times to repeat this activity + */ + public int getLoopCount() { + return loopCount; + } + + /** + * Set the number of times the activity should automatically reschedule + * itself after it has finished. + * + * @param loopCount number of times to repeat this activity + */ + public void setLoopCount(final int loopCount) { + this.loopCount = loopCount; + } + + /** + * Return true if the activity is executing its first loop. Subclasses + * normally initialize their source state on the first loop. + * + * @return true if executing first loop + */ + public boolean getFirstLoop() { + return firstLoop; + } + + /** + * Set if the activity is executing its first loop. Subclasses normally + * initialize their source state on the first loop. This method will rarely + * need to be called, unless your are reusing activities. + * + * @param firstLoop true if executing first loop + */ + public void setFirstLoop(final boolean firstLoop) { + this.firstLoop = firstLoop; + } + + /** + * Returns whether this interpolation accelerates and then decelerates as it + * interpolates. + * + * @return true if accelerations are being applied apply + */ + public boolean getSlowInSlowOut() { + return slowInSlowOut; + } + + /** + * Sets whether this interpolation accelerates and then decelerates as it + * interpolates. + * + * @param isSlowInSlowOut true if this interpolation inovolves some + * accelerations + */ + public void setSlowInSlowOut(final boolean isSlowInSlowOut) { + slowInSlowOut = isSlowInSlowOut; + } + + // **************************************************************** + // Stepping - Instead of overriding the step methods subclasses + // of this activity will normally override setRelativeTargetValue(). + // This method will be called for every step of the activity with + // a value ranging from 0,0 (for the first step) to 1.0 (for the + // final step). See PTransformActivity for an example. + // **************************************************************** + + /** + * Called when activity is started. Makes sure target value is set properly + * for start of activity. + */ + protected void activityStarted() { + super.activityStarted(); + setRelativeTargetValueAdjustingForMode(0); + } + + /** + * Called at each step of the activity. Sets the current position taking + * mode into account. + * + * @param elapsedTime number of milliseconds since the activity began + */ + + protected void activityStep(final long elapsedTime) { + super.activityStep(elapsedTime); + + float t = elapsedTime / (float) getDuration(); + + t = Math.min(1, t); + t = Math.max(0, t); + + if (getSlowInSlowOut()) { + t = computeSlowInSlowOut(t); + } + + setRelativeTargetValueAdjustingForMode(t); + } + + /** + * Called whenever the activity finishes. Reschedules it if the value of + * loopCount is > 0. + */ + protected void activityFinished() { + setRelativeTargetValueAdjustingForMode(1); + super.activityFinished(); + + final PActivityScheduler scheduler = getActivityScheduler(); + if (loopCount > 1) { + if (loopCount != Integer.MAX_VALUE) { + loopCount--; + } + firstLoop = false; + setStartTime(scheduler.getRoot().getGlobalTime()); + scheduler.addActivity(this); + } + } + + /** + * Stop this activity immediately, and remove it from the activity + * scheduler. If this activity is currently running then stoppedStepping + * will be called after it has been removed from the activity scheduler. + */ + public void terminate() { + loopCount = 0; // set to zero so that we don't reschedule self. + super.terminate(); + } + + /** + * Subclasses should override this method and set the value on their target + * (the object that they are modifying) accordingly. + * + * @param zeroToOne relative completion of task. + */ + public void setRelativeTargetValue(final float zeroToOne) { + } + + /** + * Computes percent or linear interpolation to apply when taking + * acceleration into account. + * + * @param zeroToOne Percentage of activity completed + * @return strength of acceleration + */ + public float computeSlowInSlowOut(final float zeroToOne) { + if (zeroToOne < 0.5f) { + return 2.0f * zeroToOne * zeroToOne; + } + else { + final float complement = 1.0f - zeroToOne; + return 1.0f - 2.0f * complement * complement; + } + } + + /** + * Assigns relative target value taking the mode into account. + * + * @param zeroToOne Percentage of activity completed + */ + protected void setRelativeTargetValueAdjustingForMode(final float zeroToOne) { + final float adjustedZeroToOne; + switch (mode) { + case DESTINATION_TO_SOURCE: + adjustedZeroToOne = 1 - zeroToOne; + break; + + case SOURCE_TO_DESTINATION_TO_SOURCE: + if (zeroToOne <= 0.5f) { + adjustedZeroToOne = zeroToOne * 2; + } + else { + adjustedZeroToOne = 2 * (1 - zeroToOne); + } + break; + case SOURCE_TO_DESTINATION: + default: + // Just treat the zeroToOne as how far along the interpolation + // we are. + adjustedZeroToOne = zeroToOne; + } + + setRelativeTargetValue(adjustedZeroToOne); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/activities/PTransformActivity.java b/src/main/java/edu/umd/cs/piccolo/activities/PTransformActivity.java new file mode 100644 index 0000000..56577f6 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/activities/PTransformActivity.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.activities; + +import java.awt.geom.AffineTransform; + +import edu.umd.cs.piccolo.util.PAffineTransform; + +/** + * PTransformActivity interpolates between two transforms setting its + * target's transform as it goes. See PNode. animate*() for an example of this + * activity in used. The source transform is retrieved from the target just + * before the animation is scheduled to start. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PTransformActivity extends PInterpolatingActivity { + private static final PAffineTransform STATIC_TRANSFORM = new PAffineTransform(); + + private final double[] source; + private double[] destination; + private final Target target; + + /** + * Target Objects that want to get transformed by the transform + * activity must implement this interface. See PNode.animateToTransform() + * for one way to do this. + */ + public interface Target { + + /** + * This will be called by the transform activity for each new transform + * that it computes while it is stepping. + * + * @param aTransform the transform to be applied to the target. + */ + void setTransform(AffineTransform aTransform); + + /** + * This method is called right before the transform activity starts. + * That way an object is always animated from its current position. + * + * @param aSource array to be populated with the target's gurrent matrix + */ + void getSourceMatrix(double[] aSource); + } + + /** + * Constructs a transform activity that will last for the specified + * duration, will update at the given step rate and will be applied to the + * target. + * + * Requires that the developer follow up with a setDestinationTransform + * call, otherwise the transition is undefined. + * + * @param duration duration in milliseconds of the entire activity + * @param stepRate interval in milliseconds between successive animation + * steps + * @param target the target of the activity + */ + public PTransformActivity(final long duration, final long stepRate, final Target target) { + this(duration, stepRate, target, null); + } + + /** + * Constructs a activity that will change the target's transform in the + * destination transform. It will last for the specified duration, will + * update at the given step rate. + * + * @param duration duration in milliseconds of the entire activity + * @param stepRate interval in milliseconds between successive animation + * steps + * @param target the target of the activity + * @param destination transform that the target will be after the ativity is + * finished + */ + + public PTransformActivity(final long duration, final long stepRate, final Target target, + final AffineTransform destination) { + this(duration, stepRate, 1, PInterpolatingActivity.SOURCE_TO_DESTINATION, target, destination); + } + + /** + * Create a new PTransformActivity. + *
+ * + * @param duration the length of one loop of the activity + * @param stepRate the amount of time between steps of the activity + * @param loopCount number of times the activity should reschedule itself + * @param mode defines how the activity interpolates between states + * @param target the object that the activity will be applied to and where + * the source state will be taken from. + * @param destination the destination color state + */ + public PTransformActivity(final long duration, final long stepRate, final int loopCount, final int mode, + final Target target, final AffineTransform destination) { + super(duration, stepRate, loopCount, mode); + source = new double[6]; + this.destination = new double[6]; + this.target = target; + if (destination != null) { + destination.getMatrix(this.destination); + } + } + + /** + * Whether each step invalidates paint. + * + * @return true since a node transform affects it's node's display + */ + protected boolean isAnimation() { + return true; + } + + /** + * Return the final transform that will be set on the transform activities + * target when the transform activity stops stepping. + * + * @return returns the final transform as an array of doubles + */ + public double[] getDestinationTransform() { + if (destination == null) { + return null; + } + else { + return (double[]) destination.clone(); + } + } + + /** + * Set the final transform that will be set on the transform activities + * target when the transform activity stops stepping. + * + * @param newDestination an array of doubles representing the destination + * transform + */ + public void setDestinationTransform(final double[] newDestination) { + if (newDestination == null) { + destination = null; + } + else { + destination = (double[]) newDestination.clone(); + } + } + + /** + * Is invoked when the activity is started. Ensures that setTransform is + * called on the target even before the first step. + */ + protected void activityStarted() { + if (getFirstLoop()) { + target.getSourceMatrix(source); + } + super.activityStarted(); + } + + /** + * Set's the target value to be the interpolation between the source and + * destination transforms. + * + * A value of 0 for zeroToOne means that the target should have the source + * transform. A value of 1 for zeroToOne means that the target should have + * the destination transform. + * + * @param zeroToOne how far along the activity has progressed. 0 = not at + * all, 1 = completed + */ + public void setRelativeTargetValue(final float zeroToOne) { + super.setRelativeTargetValue(zeroToOne); + + STATIC_TRANSFORM.setTransform(source[0] + zeroToOne * (destination[0] - source[0]), source[1] + zeroToOne + * (destination[1] - source[1]), source[2] + zeroToOne * (destination[2] - source[2]), source[3] + + zeroToOne * (destination[3] - source[3]), source[4] + zeroToOne * (destination[4] - source[4]), + source[5] + zeroToOne * (destination[5] - source[5])); + + target.setTransform(STATIC_TRANSFORM); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/activities/package.html b/src/main/java/edu/umd/cs/piccolo/activities/package.html new file mode 100644 index 0000000..b857b42 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/activities/package.html @@ -0,0 +1,4 @@ +
+This package supports Piccolo activities. Activities are used to control some time +dependent aspect of Piccolo such as animation. + diff --git a/src/main/java/edu/umd/cs/piccolo/event/PBasicInputEventHandler.java b/src/main/java/edu/umd/cs/piccolo/event/PBasicInputEventHandler.java new file mode 100644 index 0000000..789ebd3 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/event/PBasicInputEventHandler.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.event; + +import java.awt.event.FocusEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; + +/** + * PBasicInputEventHandler is the standard class in Piccolo that is used + * to register for mouse and keyboard events on a PNode. Note the events that + * you get depends on the node that you have registered with. For example you + * will only get mouse moved events when the mouse is over the node that you + * have registered with, not when the mouse is over some other node. + *+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PBasicInputEventHandler implements PInputEventListener { + + private PInputEventFilter eventFilter; + + /** + * Constructs a PBasicInputEventHandler with a wide open event filter. + */ + public PBasicInputEventHandler() { + super(); + eventFilter = new PInputEventFilter(); + } + + /** + * Dispatches a generic event to a more specific method. Sparing subclasses + * from the dispatch logic. + * + * @param event the event to be dispatched + * @param type Swing event type of the underlying Swing event + */ + public void processEvent(final PInputEvent event, final int type) { + if (!acceptsEvent(event, type)) { + return; + } + + switch (type) { + case KeyEvent.KEY_PRESSED: + keyPressed(event); + break; + + case KeyEvent.KEY_RELEASED: + keyReleased(event); + break; + + case KeyEvent.KEY_TYPED: + keyTyped(event); + break; + + case MouseEvent.MOUSE_CLICKED: + mouseClicked(event); + break; + + case MouseEvent.MOUSE_DRAGGED: + mouseDragged(event); + break; + + case MouseEvent.MOUSE_ENTERED: + mouseEntered(event); + break; + + case MouseEvent.MOUSE_EXITED: + mouseExited(event); + break; + + case MouseEvent.MOUSE_MOVED: + mouseMoved(event); + break; + + case MouseEvent.MOUSE_PRESSED: + mousePressed(event); + break; + + case MouseEvent.MOUSE_RELEASED: + mouseReleased(event); + break; + + case MouseWheelEvent.WHEEL_UNIT_SCROLL: + mouseWheelRotated(event); + break; + + case MouseWheelEvent.WHEEL_BLOCK_SCROLL: + mouseWheelRotatedByBlock(event); + break; + + case FocusEvent.FOCUS_GAINED: + keyboardFocusGained(event); + break; + + case FocusEvent.FOCUS_LOST: + keyboardFocusLost(event); + break; + + default: + throw new RuntimeException("Bad Event Type"); + } + } + + // **************************************************************** + // Event Filter - All this event listener can be associated with a event + // filter. The filter accepts and rejects events based on their modifier + // flags and type. If the filter is null (the + // default case) then it accepts all events. + // **************************************************************** + + /** + * Returns true if the event would be dispatched if passed to processEvent. + * + * @param event event being tested for acceptance + * @param type Swing event type of underlying swing event + * + * @return true if the event would be dispatched + */ + public boolean acceptsEvent(final PInputEvent event, final int type) { + return eventFilter.acceptsEvent(event, type); + } + + /** + * Returns the event filter responsible for filtering incoming events. + * + * @return this handler's InputEventFilter + */ + public PInputEventFilter getEventFilter() { + return eventFilter; + } + + /** + * Changes this event handler's filter to the one provided. + * + * @param newEventFilter filter to use for this input event handler + */ + public void setEventFilter(final PInputEventFilter newEventFilter) { + eventFilter = newEventFilter; + } + + /** + * Will get called whenever a key has been pressed down. Subclasses should + * override this method to implement their own behavior. + * + * @param event the event representing the keystroke + */ + public void keyPressed(final PInputEvent event) { + } + + /** + * Will get called whenever a key has been released. Subclasses should + * override this method to implement their own behavior. + * + * @param event the event representing the keystroke + */ + public void keyReleased(final PInputEvent event) { + } + + /** + * Will be called at the end of a full keystroke (down then up). Subclasses + * should override this method to implement their own behavior. + * + * @param event object which can be queried for the event's details + */ + public void keyTyped(final PInputEvent event) { + } + + /** + * Will be called at the end of a full click (mouse pressed followed by + * mouse released). Subclasses should override this method to implement + * their own behavior. + * + * @param event object which can be queried for the event's details + */ + public void mouseClicked(final PInputEvent event) { + } + + /** + * Will be called when a mouse button is pressed down. Should two buttons be + * pressed simultaneously, it will dispatch two of these in an unspecified + * order. Subclasses should override this method to implement their own + * behavior. + * + * @param event object which can be queried for the event's details + */ + public void mousePressed(final PInputEvent event) { + } + + /** + * Will be called when a drag is occurring. This is system dependent. + * Subclasses should override this method to implement their own behavior. + * + * @param event object which can be queried for the event's details + */ + public void mouseDragged(final PInputEvent event) { + } + + /** + * Will be invoked when the mouse enters a specified region. Subclasses + * should override this method to implement their own behavior. + * + * @param event object which can be queried for the event's details + */ + public void mouseEntered(final PInputEvent event) { + } + + /** + * Will be invoked when the mouse leaves a specified region. Subclasses + * should override this method to implement their own behavior. + * + * @param event object which can be queried for the event's details + */ + public void mouseExited(final PInputEvent event) { + } + + /** + * Will be called when the mouse is moved. Subclasses should override this + * method to implement their own behavior. + * + * @param event object which can be queried for event details + */ + public void mouseMoved(final PInputEvent event) { + } + + /** + * Will be called when any mouse button is released. Should two or more + * buttons be released simultaneously, this method will be called multiple + * times. Subclasses should override this method to implement their own + * behavior. + * + * @param event object which can be queried for event details + */ + public void mouseReleased(final PInputEvent event) { + } + + /** + * This method is invoked when the mouse wheel is rotated. Subclasses should + * override this method to implement their own behavior. + * + * @param event an object that can be queries to discover the event's + * details + */ + public void mouseWheelRotated(final PInputEvent event) { + } + + /** + * This method is invoked when the mouse wheel is rotated by a block. + * Subclasses should override this method to implement their own behavior. + * + * @param event an object that can be queries to discover the event's + * details + */ + public void mouseWheelRotatedByBlock(final PInputEvent event) { + } + + /** + * This method is invoked when a node gains the keyboard focus. Subclasses + * should override this method to implement their own behavior. + * + * @param event an object that can be queries to discover the event's + * details + */ + public void keyboardFocusGained(final PInputEvent event) { + } + + /** + * This method is invoked when a node loses the keyboard focus. Subclasses + * should override this method to implement their own behavior. + * + * @param event an object that can be queries to discover the event's + * details + */ + public void keyboardFocusLost(final PInputEvent event) { + } + + /** + * @deprecated see http://code.google.com/p/piccolo2d/issues/detail?id=99 + * + * @return empty string since this method is deprecated + */ + protected String paramString() { + return ""; + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/event/PDragEventHandler.java b/src/main/java/edu/umd/cs/piccolo/event/PDragEventHandler.java new file mode 100644 index 0000000..4ba2e10 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/event/PDragEventHandler.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.event; + +import java.awt.event.InputEvent; + +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.util.PDimension; + +/** + * PDragEventHandler is a simple event handler for dragging a node on the + * canvas. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PDragEventHandler extends PDragSequenceEventHandler { + + private PNode draggedNode; + private boolean moveToFrontOnPress; + + /** + * Constructs a drag event handler which defaults to not moving the node to + * the front on drag. + */ + public PDragEventHandler() { + draggedNode = null; + moveToFrontOnPress = false; + + setEventFilter(new PInputEventFilter(InputEvent.BUTTON1_MASK)); + } + + /** + * Returns the node that is currently being dragged, or null if none. + * + * @return node being dragged or null + */ + protected PNode getDraggedNode() { + return draggedNode; + } + + /** + * Set's the node that is currently being dragged. + * + * @param draggedNode node to be flagged as this handler's current drag node + */ + protected void setDraggedNode(final PNode draggedNode) { + this.draggedNode = draggedNode; + } + + /** + * Returns whether the given event should be start a drag interaction. + * + * @param event the event being tested + * + * @return true if event is a valid start drag event + */ + protected boolean shouldStartDragInteraction(final PInputEvent event) { + return super.shouldStartDragInteraction(event) && event.getPickedNode() != event.getTopCamera(); + } + + /** + * Starts a drag event and moves the dragged node to the front if this + * handler has been directed to do so with a call to setMoveToFrontOnDrag. + * + * @param event The Event responsible for the start of the drag + */ + protected void startDrag(final PInputEvent event) { + super.startDrag(event); + draggedNode = event.getPickedNode(); + if (moveToFrontOnPress) { + draggedNode.moveToFront(); + } + } + + /** + * Moves the dragged node in proportion to the drag distance. + * + * @param event event representing the drag + */ + protected void drag(final PInputEvent event) { + super.drag(event); + final PDimension d = event.getDeltaRelativeTo(draggedNode); + draggedNode.localToParent(d); + draggedNode.offset(d.getWidth(), d.getHeight()); + } + + /** + * Clears the current drag node. + * + * @param event Event reponsible for the end of the drag. Usually a + * "Mouse Up" event. + */ + protected void endDrag(final PInputEvent event) { + super.endDrag(event); + draggedNode = null; + } + + /** + * Returns whether this drag event handler has been informed to move nodes + * to the front of all other on drag. + * + * @return true if dragging a node will move it to the front + */ + public boolean getMoveToFrontOnPress() { + return moveToFrontOnPress; + } + + /** + * Informs this drag event handler whether it should move nodes to the front + * when they are dragged. Default is false. + * + * @param moveToFrontOnPress true if dragging a node should move it to the + * front + */ + public void setMoveToFrontOnPress(final boolean moveToFrontOnPress) { + this.moveToFrontOnPress = moveToFrontOnPress; + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/event/PDragSequenceEventHandler.java b/src/main/java/edu/umd/cs/piccolo/event/PDragSequenceEventHandler.java new file mode 100644 index 0000000..99e1f04 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/event/PDragSequenceEventHandler.java @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.event; + +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; + +import edu.umd.cs.piccolo.activities.PActivity; +import edu.umd.cs.piccolo.util.PUtil; + +/** + * PDragSequenceEventHandler is designed to support mouse pressed, + * dragged, and released interaction sequences. Support is also provided for + * running a continuous activity during the drag sequence. + *
+ * PDragSequenceEventHandler should be subclassed by a concrete event handler + * that implements a particular interaction. See PPanEventHandler, + * PZoomEventHandler, and PDragEventHandler for examples. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public abstract class PDragSequenceEventHandler extends PBasicInputEventHandler { + + private double minDragStartDistance = 0; + private transient boolean isDragging = false; + private transient Point2D mousePressedCanvasPoint; + private transient PActivity dragActivity; + private transient PInputEvent dragEvent; + private transient int sequenceInitiatedButton = MouseEvent.NOBUTTON; + + /** Constructs a drag sequence event handler instance. */ + public PDragSequenceEventHandler() { + } + + /** + * Returns true if this event handler is in the process of handling a drag. + * + * @return true if handling a drag + */ + public boolean isDragging() { + return isDragging; + } + + /** + * Used to inform this handler that it is in the process of handling a drag. + * + * @param isDragging true if handler is processing a drag + */ + public void setIsDragging(final boolean isDragging) { + this.isDragging = isDragging; + } + + /** + * Returns the minimum distance (in screen coordinates) before a pressed + * mouse movement is registered as a drag event. The smaller this value the + * more clicks will be incorrectly recognized as drag events. + * + * @return minimum distance a pressed mouse must move before it is + * registered as a drag + */ + public double getMinDragStartDistance() { + return minDragStartDistance; + } + + /** + * Set the minimum distance that the mouse should be dragged (in screen + * coordinates) before a new drag sequence is initiate. + * + * @param minDistance in screen coordinates + */ + public void setMinDragStartDistance(final double minDistance) { + minDragStartDistance = minDistance; + } + + /** + * Return the point in canvas coordinates where the mouse was last pressed. + * + * @return point in canvas coordinates of last mouse press + */ + public Point2D getMousePressedCanvasPoint() { + if (mousePressedCanvasPoint == null) { + mousePressedCanvasPoint = new Point2D.Double(); + } + return mousePressedCanvasPoint; + } + + // **************************************************************** + // Dragging - Methods to indicate the stages of the drag sequence. + // **************************************************************** + + /** + * Subclasses should override this method to get notified of the start of a + * new drag sequence. Note that that overriding methods must still call + * super.startDrag() for correct behavior. + * + * @param event event that started the drag sequence + */ + protected void startDrag(final PInputEvent event) { + dragEvent = event; + startDragActivity(event); + setIsDragging(true); + event.getComponent().setInteracting(true); + } + + /** + * Subclasses should override this method to get notified of the drag events + * in a drag sequence. Note that that overriding methods must still call + * super.startDrag() for correct behavior. + * + * @param event event that caused the drag + */ + protected void drag(final PInputEvent event) { + dragEvent = event; + } + + /** + * Subclasses should override this method to get notified of the end event + * in a drag sequence. Note that that overriding methods must still call + * super.startDrag() for correct behavior. + * + * @param event event that ended the drag sequence + */ + protected void endDrag(final PInputEvent event) { + stopDragActivity(event); + dragEvent = null; + event.getComponent().setInteracting(false); + setIsDragging(false); + } + + /** + * Returns true if the provided event represents a valid start for a drag + * sequence. + * + * Subclasses should override this method to add criteria for the start of a + * drag sequence. Subclasses are still responsible for calling + * super.shouldStartDragInteraction() + * + * @param event event being tested + * @return true if provided event is a good start to a drag sequence + */ + protected boolean shouldStartDragInteraction(final PInputEvent event) { + return getMousePressedCanvasPoint().distance(event.getCanvasPosition()) >= getMinDragStartDistance(); + } + + // **************************************************************** + // Drag Activity - Used for scheduling an activity during a drag + // sequence. For example zooming and auto panning are implemented + // using this. + // **************************************************************** + + /** + * Returns the scheduled activity that's updating the scene as a result to + * the current drag activity (if any). + * + * @return scheduled activity that's updating the scene as a result to the + * drag activity + */ + protected PActivity getDragActivity() { + return dragActivity; + } + + /** + * Schedules the "infinite" drag activity so that auto-panning and zooming + * will continue to update the scene even if there are no further drag + * events fired. For example, if the mouse is dragged to the right while + * pressing the right mouse button and then paused for a while, the scene + * should continue to zoom in. + * + * @param event the event that's responsible for the start of the activity + */ + protected void startDragActivity(final PInputEvent event) { + dragActivity = new PActivity(-1, PUtil.DEFAULT_ACTIVITY_STEP_RATE); + dragActivity.setDelegate(new PActivity.PActivityDelegate() { + public void activityStarted(final PActivity activity) { + dragActivityFirstStep(dragEvent); + } + + public void activityStepped(final PActivity activity) { + dragActivityStep(dragEvent); + } + + public void activityFinished(final PActivity activity) { + dragActivityFinalStep(dragEvent); + } + }); + + event.getCamera().getRoot().addActivity(dragActivity); + } + + /** + * Stops the activity responsible for updating the scene. + * + * @param event The event responsible for stopping the drag activity + */ + protected void stopDragActivity(final PInputEvent event) { + dragActivity.terminate(); + dragActivity = null; + } + + /** + * Subclasses override this method to get notified when the drag activity + * starts stepping. + * + * @param event the event responsible for the first step in the drag + * activity + */ + protected void dragActivityFirstStep(final PInputEvent event) { + } + + /** + * During a drag sequence an activity is scheduled that runs continuously + * while the drag sequence is active. This can be used to support some + * additional behavior that is not driven directly by mouse events. For + * example PZoomEventHandler uses it for zooming and PPanEventHandler uses + * it for auto panning. + * + * @param event the event encapsulating the callback context for the + * activity step + */ + protected void dragActivityStep(final PInputEvent event) { + } + + /** + * Subclasses should override this method to get notified when the drag + * activity stops stepping. + * + * @param aEvent the event responsible for ending the activity + */ + protected void dragActivityFinalStep(final PInputEvent aEvent) { + } + + /** + * Subclasses should not override this method, instead they should + * override the appropriate drag callbacks. + * + * @param event The event to be queried about the details of the mouse press + */ + public void mousePressed(final PInputEvent event) { + super.mousePressed(event); + + if (sequenceInitiatedButton == MouseEvent.NOBUTTON) { + sequenceInitiatedButton = event.getButton(); + + getMousePressedCanvasPoint().setLocation(event.getCanvasPosition()); + if (!isDragging() && shouldStartDragInteraction(event)) { + startDrag(event); + } + } + } + + /** + * Subclasses should not override this method, instead they should + * override the appropriate drag method. + * + * @param event The event to be queried about the details of the mouse press + */ + public void mouseDragged(final PInputEvent event) { + super.mouseDragged(event); + + if (sequenceInitiatedButton != MouseEvent.NOBUTTON) { + if (!isDragging()) { + if (shouldStartDragInteraction(event)) { + startDrag(event); + } + return; + } + drag(event); + } + } + + /** + * Subclasses should not override this method, instead they should + * override the appropriate drag method. + * + * @param event The event to be queried about the details of the mouse release + */ + public void mouseReleased(final PInputEvent event) { + super.mouseReleased(event); + if (sequenceInitiatedButton == event.getButton()) { + if (isDragging()) { + endDrag(event); + } + sequenceInitiatedButton = MouseEvent.NOBUTTON; + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/event/PInputEvent.java b/src/main/java/edu/umd/cs/piccolo/event/PInputEvent.java new file mode 100644 index 0000000..01c51ac --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/event/PInputEvent.java @@ -0,0 +1,641 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.event; + +import java.awt.Cursor; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.geom.Point2D; + +import javax.swing.SwingUtilities; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PComponent; +import edu.umd.cs.piccolo.PInputManager; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.util.PDimension; +import edu.umd.cs.piccolo.util.PPickPath; + +/** + * PInputEvent is used to notify PInputEventListeners of keyboard and + * mouse input. It has methods for normal event properties such as event + * modifier keys and event canvas location. + *
+ * In addition is has methods to get the mouse position and delta in a variety + * of coordinate systems. + *
+ * Last of all it provides access to the dispatch manager that can be queried to + * find the current mouse over, mouse focus, and keyboard focus. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PInputEvent { + /** The underlying Swing Event. */ + private final InputEvent inputEvent; + + /** Path relating to the current mouse event. */ + private PPickPath pickPath; + + /** Input manager responsible for the creation of this event. */ + private final PInputManager inputManager; + + /** Flag used to identify this event as handled. */ + private boolean handled; + + /** + * Create an event with the given inputManager and based on the given swing + * event. + * + * @param inputManager source of PInputEvent + * @param event underlying swing event + */ + public PInputEvent(final PInputManager inputManager, final InputEvent event) { + inputEvent = event; + this.inputManager = inputManager; + } + + /** + * Changes the cursor to the one provided and stores it on the cursor stack + * for later retrieval. + * + * @param cursor cursor to push on cursor stack + */ + public void pushCursor(final Cursor cursor) { + final PComponent component = getTopCamera().getComponent(); + component.pushCursor(cursor); + } + + /** + * Removes the top most cursor from the cursor stack and sets it as the + * current cursor. + */ + public void popCursor() { + final PComponent component = getTopCamera().getComponent(); + component.popCursor(); + } + + // **************************************************************** + // Accessing Picked Objects - Methods to access the objects associated + // with this event. + //
+ // Cameras can view layers that have + // other cameras on them, so events may be arriving through a stack + // of many cameras. The getCamera() method returns the bottommost + // camera on that stack. The getTopCamera method returns the topmost + // camera on that stack, this is also the camera through which the + // event originated. + // **************************************************************** + + /** + * Return the bottom most camera that is currently painting. If you are + * using internal cameras this may be different then what is returned by + * getTopCamera. + * + * @return the current PickPath's bottom camera. + */ + public PCamera getCamera() { + return getPath().getBottomCamera(); + } + + /** + * Return the topmost camera this is painting. This is the camera associated + * with the PCanvas that requested the current repaint. + * + * @return topmost camera on the pick path + */ + public PCamera getTopCamera() { + return getPath().getTopCamera(); + } + + /** + * Get the canvas associated with the top camera. This is the canvas where + * the originating swing event came from. + * + * @return component attached to the top camera of the current pick path + */ + public PComponent getComponent() { + return getTopCamera().getComponent(); + } + + /** + * Return the input manager that dispatched this event. You can use this + * input manager to find the current mouse focus, mouse over, and key focus + * nodes. You can also set a new key focus node. + * + * @return input manager that dispatched this event + */ + public PInputManager getInputManager() { + return inputManager; + } + + /** + * Return the PPickPath associated with this input event. + * + * @return pick path associated with this event (may be null) + */ + public PPickPath getPath() { + return pickPath; + } + + /** + * Sets the PIckPath associated with this mouse event. + * + * @param path path to associate with this mouse event + */ + public void setPath(final PPickPath path) { + pickPath = path; + } + + /** + * Return the bottom node on the current pickpath, that is the picked node + * furthest from the root node. + * + * @return the currently picked node of this mouse event + */ + public PNode getPickedNode() { + if (pickPath == null) { + return null; + } + return pickPath.getPickedNode(); + } + + // **************************************************************** + // Basics + // **************************************************************** + + /** + * Returns the key code associated with a key event. + * + * @return key code associated with a key event + */ + public int getKeyCode() { + if (isKeyEvent()) { + final KeyEvent e = (KeyEvent) inputEvent; + return e.getKeyCode(); + } + throw new IllegalStateException("Can't get keycode from mouse event"); + } + + /** + * Returns the character associated with a key event. + * + * @return char associated with a key event + */ + public char getKeyChar() { + if (isKeyEvent()) { + final KeyEvent e = (KeyEvent) inputEvent; + return e.getKeyChar(); + } + throw new IllegalStateException("Can't get keychar from mouse event"); + } + + /** + * Returns the location on the keyboard from which the key stroke + * originated. + * + * @return location on keyboard from which stroke originated. + */ + public int getKeyLocation() { + if (isKeyEvent()) { + final KeyEvent e = (KeyEvent) inputEvent; + return e.getKeyLocation(); + } + throw new IllegalStateException("Can't get keylocation from mouse event"); + } + + /** + * Returns whether the key event involves the action key. + * + * @return true if key involved is the action key + */ + public boolean isActionKey() { + if (isKeyEvent()) { + final KeyEvent e = (KeyEvent) inputEvent; + return e.isActionKey(); + } + throw new IllegalStateException("Can't get isActionKey from mouse event"); + } + + /** + * Returns the modifiers provided for the input event by swing. + * + * @return modifier flags for the input event + */ + public int getModifiers() { + if (!isFocusEvent()) { + return inputEvent.getModifiers(); + } + throw new IllegalStateException("Can't get modifiers from focus event"); + } + + /** + * Returns the extended modifiers provided for the input event by swing. + * + * @return extended modifies of input event + */ + public int getModifiersEx() { + if (!isFocusEvent()) { + return inputEvent.getModifiersEx(); + } + throw new IllegalStateException("Can't get modifiers ex from focus event"); + } + + /** + * Returns the click count of the mouse event. + * + * @return click count of mouse event + */ + public int getClickCount() { + if (isMouseEvent()) { + return ((MouseEvent) inputEvent).getClickCount(); + } + throw new IllegalStateException("Can't get clickcount from key event"); + } + + /** + * Returns the time at which the event was emitted. + * + * @return time at which the vent was emitted + */ + public long getWhen() { + if (!isFocusEvent()) { + return inputEvent.getWhen(); + } + throw new IllegalStateException("Can't get when from focus event"); + } + + /** + * Returns whether the alt key is currently down. + * + * @return true if alt key is down + */ + public boolean isAltDown() { + if (!isFocusEvent()) { + return inputEvent.isAltDown(); + } + throw new IllegalStateException("Can't get altdown from focus event"); + } + + /** + * Returns whether the control key is currently down. + * + * @return true if control key is down + */ + public boolean isControlDown() { + if (!isFocusEvent()) { + return inputEvent.isControlDown(); + } + throw new IllegalStateException("Can't get controldown from focus event"); + } + + /** + * Returns whether the meta key is currently down. + * + * @return true if meta key is down + */ + public boolean isMetaDown() { + if (!isFocusEvent()) { + return inputEvent.isMetaDown(); + } + throw new IllegalStateException("Can't get modifiers from focus event"); + } + + /** + * Returns whether the shift key is currently down. + * + * @return true if shift key is down + */ + public boolean isShiftDown() { + if (!isFocusEvent()) { + return inputEvent.isShiftDown(); + } + throw new IllegalStateException("Can't get shiftdown from focus event"); + } + + /** + * Returns whether the mouse event involves the left mouse button. + * + * @return true if left mouse button is involved the mouse event + */ + public boolean isLeftMouseButton() { + if (isMouseEvent()) { + return SwingUtilities.isLeftMouseButton((MouseEvent) getSourceSwingEvent()); + } + throw new IllegalStateException("Can't get isLeftMouseButton from focus event"); + } + + /** + * Returns whether the mouse event involves the middle mouse button. + * + * @return true if middle mouse button is involved the mouse event + */ + public boolean isMiddleMouseButton() { + if (isMouseEvent()) { + return SwingUtilities.isMiddleMouseButton((MouseEvent) getSourceSwingEvent()); + } + throw new IllegalStateException("Can't get isMiddleMouseButton from focus event"); + } + + /** + * Returns whether the mouse event involves the right mouse button. + * + * @return true if right mouse button is involved the mouse event + */ + public boolean isRightMouseButton() { + if (isMouseEvent()) { + return SwingUtilities.isRightMouseButton((MouseEvent) getSourceSwingEvent()); + } + throw new IllegalStateException("Can't get isRightMouseButton from focus event"); + } + + /** + * Return true if another event handler has already handled this event. + * Event handlers should use this as a hint before handling the event + * themselves and possibly reject events that have already been handled. + * + * @return true if event has been marked as handled + */ + public boolean isHandled() { + return handled; + } + + /** + * Set that this event has been handled by an event handler. This is a + * relaxed for of consuming events. The event will continue to get + * dispatched to event handlers even after it is marked as handled, but + * other event handlers that might conflict are expected to ignore events + * that have already been handled. + * + * @param handled whether the event is marked + */ + public void setHandled(final boolean handled) { + this.handled = handled; + } + + /** + * Returns the mouse button value of the underlying mouse event. + * + * @return button value of underlying mouse event + */ + public int getButton() { + if (isMouseEvent()) { + return ((MouseEvent) inputEvent).getButton(); + } + throw new IllegalStateException("Can't get button from key event"); + } + + /** + * Returns the current value of the wheel rotation on Mouse Wheel Rotation + * events. + * + * @return wheel rotation value + */ + public int getWheelRotation() { + if (isMouseWheelEvent()) { + return ((MouseWheelEvent) inputEvent).getWheelRotation(); + } + throw new IllegalStateException("Can't get wheel rotation from non-wheel event"); + } + + /** + * Returns the underlying swing event that this PInputEvent is wrapping. + * + * @return underlying swing event + */ + public InputEvent getSourceSwingEvent() { + return inputEvent; + } + + // **************************************************************** + // Classification - Methods to distinguish between mouse and key + // events. + // **************************************************************** + + /** + * Returns whether the underlying event is a KeyEvent. + * + * @return true if is key event + */ + public boolean isKeyEvent() { + return inputEvent instanceof KeyEvent; + } + + /** + * Returns whether the underlying event is a MouseEvent. + * + * @return true if is mouse event + */ + public boolean isMouseEvent() { + return inputEvent instanceof MouseEvent; + } + + /** + * Returns whether the underlying event is a Mouse Wheel Event. + * + * @return true if is a mouse wheel event + */ + + public boolean isMouseWheelEvent() { + return inputEvent instanceof MouseWheelEvent; + } + + /** + * Returns whether the underlying event is a Focus Event. + * + * @return true if is focus event + */ + public boolean isFocusEvent() { + return inputEvent == null; + } + + /** + * Returns whether the underlying event is a mouse entered or exited event. + * + * @return true if is a mouse entered or exited event + */ + public boolean isMouseEnteredOrMouseExited() { + if (isMouseEvent()) { + return inputEvent.getID() == MouseEvent.MOUSE_ENTERED || inputEvent.getID() == MouseEvent.MOUSE_EXITED; + } + return false; + } + + /** + * Returns whether or not this event is a popup menu trigger event for the + * platform. Must not be called if this event isn't a mouse event. + *
+ * Note: Popup menus are triggered differently on different systems.
+ * Therefore, isPopupTrigger
should be checked in both
+ * mousePressed
and mouseReleased
for proper
+ * cross-platform functionality.
+ *
+ * @return boolean, true if this event triggers a popup menu for this
+ * platform
+ */
+ public boolean isPopupTrigger() {
+ if (isMouseEvent()) {
+ return ((MouseEvent) inputEvent).isPopupTrigger();
+ }
+ throw new IllegalStateException("Can't get clickcount from key event");
+ }
+
+ // ****************************************************************
+ // Coordinate Systems - Methods for getting mouse location data
+ // These methods are only designed for use with PInputEvents that
+ // return true to the isMouseEvent method.
+ // ****************************************************************
+
+ /**
+ * Return the mouse position in PCanvas coordinates.
+ *
+ * @return mouse position in PCanvas coordinates
+ */
+ public Point2D getCanvasPosition() {
+ return (Point2D) inputManager.getCurrentCanvasPosition().clone();
+ }
+
+ /**
+ * Return the delta between the last and current mouse position in PCanvas
+ * coordinates.
+ *
+ * @return delta between last and current mouse position as measured by the
+ * PCanvas
+ */
+ public PDimension getCanvasDelta() {
+ final Point2D last = inputManager.getLastCanvasPosition();
+ final Point2D current = inputManager.getCurrentCanvasPosition();
+ return new PDimension(current.getX() - last.getX(), current.getY() - last.getY());
+ }
+
+ /**
+ * Return the mouse position relative to a given node on the pick path.
+ *
+ * @param nodeOnPath node on the current PPickPath
+ *
+ * @return mouse position relative to the provided node on pick path
+ */
+ public Point2D getPositionRelativeTo(final PNode nodeOnPath) {
+ if (pickPath == null) {
+ throw new RuntimeException("Attempting to use pickPath for a non-mouse event.");
+ }
+ final Point2D r = getCanvasPosition();
+ return pickPath.canvasToLocal(r, nodeOnPath);
+ }
+
+ /**
+ * Return the delta between the last and current mouse positions relative to
+ * a given node on the pick path.
+ *
+ * @param nodeOnPath node from which to measure
+ * @return delta between current mouse position and a given node on the pick
+ * path
+ */
+ public PDimension getDeltaRelativeTo(final PNode nodeOnPath) {
+ if (pickPath == null) {
+ throw new RuntimeException("Attempting to use pickPath for a non-mouse event.");
+ }
+ final PDimension r = getCanvasDelta();
+ return (PDimension) pickPath.canvasToLocal(r, nodeOnPath);
+ }
+
+ /**
+ * Return the mouse position transformed through the view transform of the
+ * bottom camera.
+ *
+ * @return mouse position as measured by the bottom camera
+ */
+ public Point2D getPosition() {
+ if (pickPath == null) {
+ throw new RuntimeException("Attempting to use pickPath for a non-mouse event.");
+ }
+ final Point2D r = getCanvasPosition();
+ pickPath.canvasToLocal(r, getCamera());
+ return getCamera().localToView(r);
+ }
+
+ /**
+ * Return the delta between the last and current mouse positions transformed
+ * through the view transform of the bottom camera.
+ *
+ * @return delta between last and current mouse position as measured by the
+ * bottom camera
+ */
+ public PDimension getDelta() {
+ if (pickPath == null) {
+ throw new RuntimeException("Attempting to use pickPath for a non-mouse event.");
+ }
+ final PDimension r = getCanvasDelta();
+ pickPath.canvasToLocal(r, getCamera());
+ return (PDimension) getCamera().localToView(r);
+ }
+
+ /**
+ * Returns a string representation of this object for debugging purposes.
+ *
+ * @return string representation of this object
+ */
+ public String toString() {
+ final StringBuffer result = new StringBuffer();
+
+ result.append(super.toString().replaceAll(".*\\.", ""));
+ result.append('[');
+ if (handled) {
+ result.append("handled");
+ }
+ result.append(']');
+
+ return result.toString();
+ }
+
+ /**
+ * Modified by miura
+ * @param satn
+ */
+ private String actionCommand;
+
+ /**
+ * Modified by miura
+ * @param s
+ */
+ public void setActionCommand(String s){
+ actionCommand = s;
+ }
+ /**
+ * Modified by miura
+ * @param satn
+ */
+ public String getActionCommand(){
+ return actionCommand;
+ }
+
+}
diff --git a/src/main/java/edu/umd/cs/piccolo/event/PInputEventFilter.java b/src/main/java/edu/umd/cs/piccolo/event/PInputEventFilter.java
new file mode 100644
index 0000000..eea28b3
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/event/PInputEventFilter.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo.event;
+
+import java.awt.event.FocusEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseWheelEvent;
+
+/**
+ * PInputEventFilter is a class that filters input events based on the
+ * events modifiers and type. Any PBasicInputEventHandler that is associated
+ * with an event filter will only receive events that pass through the filter.
+ *
+ * To be accepted events must contain all the modifiers listed in the andMask, + * at least one of the modifiers listed in the orMask, and none of the modifiers + * listed in the notMask. The event filter also lets you specify specific event + * types (mousePressed, released, ...) to accept or reject. + *
+ * If the event filter is set to consume, then it will call consume on any event + * that it successfully accepts. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PInputEventFilter { + /** Mask representing all possible modifiers. */ + public static int ALL_MODIFIERS_MASK = InputEvent.BUTTON1_MASK | InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK + | InputEvent.SHIFT_MASK | InputEvent.CTRL_MASK | InputEvent.ALT_MASK | InputEvent.ALT_GRAPH_MASK + | InputEvent.META_MASK; + + /** If event modifiers don't match this exactly, event it filtered. */ + private int andMask; + + /** If event modifiers have no bits from orMask enabled, event is filtered. */ + private int orMask; + + /** If event modifier has any of the notMask bits on, it is not accepted. */ + private int notMask; + + /** Number of clicks that an incoming event must have to be accepted. */ + private short clickCount = -1; + + /** Whether accepted events should be marked as handled. */ + private boolean marksAcceptedEventsAsHandled = false; + + /** Whether handled events should be immediately filtered. */ + private boolean acceptsAlreadyHandledEvents = false; + + /** Whether key pressed events are accepted. */ + private boolean acceptsKeyPressed = true; + + /** Whether key released events are accepted. */ + private boolean acceptsKeyReleased = true; + + /** Whether key typed events are accepted. */ + private boolean acceptsKeyTyped = true; + + /** Whether mouse clicked events are accepted. */ + private boolean acceptsMouseClicked = true; + + /** Whether mouse dragged events are accepted. */ + private boolean acceptsMouseDragged = true; + + /** Whether mouse entered events are accepted. */ + private boolean acceptsMouseEntered = true; + + /** Whether mouse exited events are accepted. */ + private boolean acceptsMouseExited = true; + + /** Whether mouse moved events are accepted. */ + private boolean acceptsMouseMoved = true; + + /** Whether mouse pressed events are accepted. */ + private boolean acceptsMousePressed = true; + + /** Whether mouse released events are accepted. */ + private boolean acceptsMouseReleased = true; + + /** Whether mouse wheel rotated events are accepted. */ + private boolean acceptsMouseWheelRotated = true; + + /** Whether focus events are accepted. */ + private boolean acceptsFocusEvents = true; + + /** + * Creates a PInputEventFilter that accepts everything. + */ + public PInputEventFilter() { + acceptEverything(); + } + + /** + * Creates a PInputEventFilter that will accept events if they have the + * given andMask. + * + * @param andMask exact pattern event modifiers must be to get accepted + */ + public PInputEventFilter(final int andMask) { + this(); + this.andMask = andMask; + } + + /** + * Creates a PInputEventFilter that will accept events if they have the + * given andMask and do not contain any of the bits in the notMask. + * + * @param andMask exact pattern event modifiers must be to get accepted + * @param notMask if any or these bits are on event is not accepted + */ + public PInputEventFilter(final int andMask, final int notMask) { + this(andMask); + this.notMask = notMask; + } + + /** + * Returns true if the passed event is one that is accepted. + * + * @param event Event under consideration + * @param type The type of event encoded as the PInputEvent + * @return true if event is accepted + */ + public boolean acceptsEvent(final PInputEvent event, final int type) { + boolean aResult = false; + int modifiers = 0; + + if (!event.isFocusEvent()) { + modifiers = event.getModifiers(); + } + + if (event.isHandled() && !acceptsAlreadyHandledEvents) { + return false; + } + + if (modifiers != 0) { + if ((modifiers & andMask) != andMask || (modifiers & notMask) != 0) { + return false; + } + + if (orMask != ALL_MODIFIERS_MASK && (modifiers & orMask) == 0) { + return false; + } + } + + if (event.isMouseEvent() && clickCount != -1 && clickCount != event.getClickCount()) { + return false; + } + + switch (type) { + case KeyEvent.KEY_PRESSED: + aResult = getAcceptsKeyPressed(); + break; + + case KeyEvent.KEY_RELEASED: + aResult = getAcceptsKeyReleased(); + break; + + case KeyEvent.KEY_TYPED: + aResult = getAcceptsKeyTyped(); + break; + + case MouseEvent.MOUSE_CLICKED: + aResult = getAcceptsMouseClicked(); + break; + + case MouseEvent.MOUSE_DRAGGED: + aResult = getAcceptsMouseDragged(); + break; + + case MouseEvent.MOUSE_ENTERED: + aResult = getAcceptsMouseEntered(); + break; + + case MouseEvent.MOUSE_EXITED: + aResult = getAcceptsMouseExited(); + break; + + case MouseEvent.MOUSE_MOVED: + aResult = getAcceptsMouseMoved(); + break; + + case MouseEvent.MOUSE_PRESSED: + aResult = getAcceptsMousePressed(); + break; + + case MouseEvent.MOUSE_RELEASED: + aResult = getAcceptsMouseReleased(); + break; + + case MouseWheelEvent.WHEEL_UNIT_SCROLL: + case MouseWheelEvent.WHEEL_BLOCK_SCROLL: + aResult = getAcceptsMouseWheelRotated(); + break; + + case FocusEvent.FOCUS_GAINED: + case FocusEvent.FOCUS_LOST: + aResult = getAcceptsFocusEvents(); + break; + + default: + throw new RuntimeException("PInputEvent with bad ID"); + } + + if (aResult && getMarksAcceptedEventsAsHandled()) { + event.setHandled(true); + } + + return aResult; + } + + /** + * Makes this filter accept all mouse click combinations. + */ + public void acceptAllClickCounts() { + clickCount = -1; + } + + /** + * Makes the filter accept all event types. + */ + public void acceptAllEventTypes() { + acceptsKeyPressed = true; + acceptsKeyReleased = true; + acceptsKeyTyped = true; + acceptsMouseClicked = true; + acceptsMouseDragged = true; + acceptsMouseEntered = true; + acceptsMouseExited = true; + acceptsMouseMoved = true; + acceptsMousePressed = true; + acceptsMouseReleased = true; + acceptsMouseWheelRotated = true; + acceptsFocusEvents = true; + } + + /** + * Makes this filter accept absolutely everything. + */ + public void acceptEverything() { + acceptAllEventTypes(); + setAndMask(0); + setOrMask(ALL_MODIFIERS_MASK); + setNotMask(0); + acceptAllClickCounts(); + } + + /** + * Returns whether this filter accepts key pressed events. + * + * @return true if filter accepts key pressed events + */ + public boolean getAcceptsKeyPressed() { + return acceptsKeyPressed; + } + + /** + * Returns whether this filter accepts key released events. + * + * @return true if filter accepts key released events + */ + public boolean getAcceptsKeyReleased() { + return acceptsKeyReleased; + } + + /** + * Returns whether this filter accepts key typed events. + * + * @return true if filter accepts key typed events + */ + public boolean getAcceptsKeyTyped() { + return acceptsKeyTyped; + } + + /** + * Returns whether this filter accepts mouse clicked events. + * + * @return true if filter accepts mouse clicked events + */ + public boolean getAcceptsMouseClicked() { + return acceptsMouseClicked; + } + + /** + * Returns whether this filter accepts mouse dragged events. + * + * @return true if filter accepts mouse dragged events + */ + public boolean getAcceptsMouseDragged() { + return acceptsMouseDragged; + } + + /** + * Returns whether this filter accepts mouse entered events. + * + * @return true if filter accepts mouse entered events + */ + public boolean getAcceptsMouseEntered() { + return acceptsMouseEntered; + } + + /** + * Returns whether this filter accepts mouse exited events. + * + * @return true if filter accepts mouse exited events + */ + public boolean getAcceptsMouseExited() { + return acceptsMouseExited; + } + + /** + * Returns whether this filter accepts mouse moved events. + * + * @return true if filter accepts mouse moved events + */ + public boolean getAcceptsMouseMoved() { + return acceptsMouseMoved; + } + + /** + * Returns whether this filter accepts mouse pressed events. + * + * @return true if filter accepts mouse pressed events + */ + public boolean getAcceptsMousePressed() { + return acceptsMousePressed; + } + + /** + * Returns whether this filter accepts mouse released events. + * + * @return true if filter accepts mouse released events + */ + public boolean getAcceptsMouseReleased() { + return acceptsMouseReleased; + } + + /** + * Returns whether this filter accepts mouse wheel rotated events. + * + * @return true if filter accepts mouse wheel rotated events + */ + public boolean getAcceptsMouseWheelRotated() { + return acceptsMouseWheelRotated; + } + + /** + * Returns whether this filter accepts focus events. + * + * @return true if filter accepts focus events + */ + public boolean getAcceptsFocusEvents() { + return acceptsFocusEvents; + } + + /** + * Returns whether this filter accepts events that have already been flagged + * as handled. + * + * @return true if filter accepts events that have already been flagged as + * handled + */ + public boolean getAcceptsAlreadyHandledEvents() { + return acceptsAlreadyHandledEvents; + } + + /** + * Returns whether this filter marks events as handled if they are accepted. + * + * @return true if filter will mark events as filtered if they are accepted + */ + public boolean getMarksAcceptedEventsAsHandled() { + return marksAcceptedEventsAsHandled; + } + + /** + * Flags all mouse click events as disallowed, regardless of button + * configuration. + */ + public void rejectAllClickCounts() { + clickCount = Short.MAX_VALUE; + } + + /** + * Configures filter so that no events will ever get accepted. By itself not + * terribly useful, but it's a more restrictive starting point than + * acceptAllEvents(); + */ + public void rejectAllEventTypes() { + acceptsKeyPressed = false; + acceptsKeyReleased = false; + acceptsKeyTyped = false; + acceptsMouseClicked = false; + acceptsMouseDragged = false; + acceptsMouseEntered = false; + acceptsMouseExited = false; + acceptsMouseMoved = false; + acceptsMousePressed = false; + acceptsMouseReleased = false; + acceptsMouseWheelRotated = false; + acceptsFocusEvents = false; + } + + /** + * Sets the number of clicks that an incoming event must have to be accepted. + * + * @param aClickCount number clicks that an incoming event must have to be accepted + */ + public void setAcceptClickCount(final short aClickCount) { + clickCount = aClickCount; + } + + /** + * Sets whether this filter accepts key pressed events. + * + * @param aBoolean whether filter should accept key pressed events + */ + public void setAcceptsKeyPressed(final boolean aBoolean) { + acceptsKeyPressed = aBoolean; + } + + /** + * Sets whether this filter accepts key released events. + * + * @param aBoolean whether filter should accept key released events + */ + public void setAcceptsKeyReleased(final boolean aBoolean) { + acceptsKeyReleased = aBoolean; + } + + /** + * Sets whether this filter accepts key typed events. + * + * @param aBoolean whether filter should accept key typed events + */ + + public void setAcceptsKeyTyped(final boolean aBoolean) { + acceptsKeyTyped = aBoolean; + } + + /** + * Sets whether this filter accepts mouse clicked events. + * + * @param aBoolean whether filter should accept mouse clicked events + */ + public void setAcceptsMouseClicked(final boolean aBoolean) { + acceptsMouseClicked = aBoolean; + } + + /** + * Sets whether this filter accepts mouse dragged events. + * + * @param aBoolean whether filter should accept mouse dragged events + */ + public void setAcceptsMouseDragged(final boolean aBoolean) { + acceptsMouseDragged = aBoolean; + } + + /** + * Sets whether this filter accepts mouse entered events. + * + * @param aBoolean whether filter should accept mouse entered events + */ + public void setAcceptsMouseEntered(final boolean aBoolean) { + acceptsMouseEntered = aBoolean; + } + + /** + * Sets whether this filter accepts mouse exited events. + * + * @param aBoolean whether filter should accept mouse exited events + */ + public void setAcceptsMouseExited(final boolean aBoolean) { + acceptsMouseExited = aBoolean; + } + + /** + * Sets whether this filter accepts mouse moved events. + * + * @param aBoolean whether filter should accept mouse moved events + */ + public void setAcceptsMouseMoved(final boolean aBoolean) { + acceptsMouseMoved = aBoolean; + } + + /** + * Sets whether this filter accepts mouse pressed events. + * + * @param aBoolean whether filter should accept mouse pressed events + */ + public void setAcceptsMousePressed(final boolean aBoolean) { + acceptsMousePressed = aBoolean; + } + + /** + * Sets whether this filter accepts mouse released events. + * + * @param aBoolean whether filter should accept mouse released events + */ + public void setAcceptsMouseReleased(final boolean aBoolean) { + acceptsMouseReleased = aBoolean; + } + + /** + * Sets whether this filter accepts mouse wheel rotation events. + * + * @param aBoolean whether filter should accept mouse wheel rotated events + */ + public void setAcceptsMouseWheelRotated(final boolean aBoolean) { + acceptsMouseWheelRotated = aBoolean; + } + + /** + * Sets whether this filter accepts focus events. + * + * @param aBoolean whether filter should accept focus events + */ + public void setAcceptsFocusEvents(final boolean aBoolean) { + acceptsFocusEvents = aBoolean; + } + + /** + * Sets and mask used to filter events. All bits of the andMask must be 1s + * for the event to be accepted. + * + * @param aAndMask the and mask to use for filtering events + */ + public void setAndMask(final int aAndMask) { + andMask = aAndMask; + } + + /** + * Sets whether already handled events should be accepted. + * + * @param aBoolean whether already handled events should be accepted + */ + public void setAcceptsAlreadyHandledEvents(final boolean aBoolean) { + acceptsAlreadyHandledEvents = aBoolean; + } + + /** + * Sets whether events will be marked as dirty once accepted. + * + * @param aBoolean whether events will be marked as dirty once accepted + */ + public void setMarksAcceptedEventsAsHandled(final boolean aBoolean) { + marksAcceptedEventsAsHandled = aBoolean; + } + + /** + * Sets not mask used to filter events. If any of the not bits are enabled, + * then the event is not accepted. + * + * @param aNotMask the not mask to use for filtering events + */ + public void setNotMask(final int aNotMask) { + notMask = aNotMask; + } + + /** + * Sets or mask used to filter events. If any of the or bits are enabled, + * then the event is accepted. + * + * @param aOrMask the or mask to use for filtering events + */ + public void setOrMask(final int aOrMask) { + orMask = aOrMask; + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/event/PInputEventListener.java b/src/main/java/edu/umd/cs/piccolo/event/PInputEventListener.java new file mode 100644 index 0000000..9cdb55e --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/event/PInputEventListener.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.event; + +import java.util.EventListener; + +/** + * PInputEventListener defines the most basic interface for objects that + * want to listen to PNodes for input events. This interface is very simple so + * that others may extend Piccolo's input management system. If you are just + * using Piccolo's default input management system then you will most often use + * PBasicInputEventHandler to register with a node for input events. + *
+ * + * @see PBasicInputEventHandler + * @version 1.0 + * @author Jesse Grosjean + */ +public interface PInputEventListener extends EventListener { + /** + * Called whenever an event is emitted. Used to notify listeners that an + * event is available for proecessing. + * + * @param event event that was emitted + * @param type type of event + */ + void processEvent(PInputEvent event, int type); +} diff --git a/src/main/java/edu/umd/cs/piccolo/event/PPanEventHandler.java b/src/main/java/edu/umd/cs/piccolo/event/PPanEventHandler.java new file mode 100644 index 0000000..b9464e8 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/event/PPanEventHandler.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.event; + +import java.awt.event.InputEvent; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PDimension; + +/** + * PPanEventHandler provides event handlers for basic panning of the + * canvas view with the left mouse. The interaction is that clicking and + * dragging the mouse translates the view so that the point on the surface stays + * under the mouse. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PPanEventHandler extends PDragSequenceEventHandler { + + private static final int DEFAULT_MAX_AUTOPAN_SPEED = 750; + private static final int DEFAULT_MIN_AUTOPAN_SPEED = 250; + + private boolean autopan; + private double minAutopanSpeed = DEFAULT_MIN_AUTOPAN_SPEED; + private double maxAutopanSpeed = DEFAULT_MAX_AUTOPAN_SPEED; + + /** + * Constructs a Pan Event Handler that will by default perform auto-panning. + */ + public PPanEventHandler() { + super(); + setEventFilter(new PInputEventFilter(InputEvent.BUTTON1_MASK)); + setAutopan(true); + } + + /** + * Updates the view in response to a user initiated drag event. + * + * @param event event responsible for the drag + */ + protected void drag(final PInputEvent event) { + super.drag(event); + pan(event); + } + + /** + * Pans the camera in response to the pan event provided. + * + * @param event contains details about the drag used to translate the view + */ + protected void pan(final PInputEvent event) { + if (event.isMetaDown()) return; // for MacOSX Touchpad with command key (emurate right-drag) miuramo 2012.9.21 + final PCamera c = event.getCamera(); + final Point2D l = event.getPosition(); + + if (c.getViewBounds().contains(l)) { + final PDimension d = event.getDelta(); + c.translateView(d.getWidth(), d.getHeight()); + } + } + + // **************************************************************** + // Auto Pan + // **************************************************************** + + /** + * Determines if auto-panning will occur or not. + * + * @param autopan true if auto-panning functionality will be active + */ + public void setAutopan(final boolean autopan) { + this.autopan = autopan; + } + + /** + * Returns whether the auto-panning functoinality is enabled. + * + * @return true if auto-panning is enabled + */ + public boolean getAutopan() { + return autopan; + } + + /** + * Set the minAutoPan speed in pixels per second. + * + * @param minAutopanSpeed number of pixels to assign as the minimum the + * autopan feature can pan the view + */ + public void setMinAutopanSpeed(final double minAutopanSpeed) { + this.minAutopanSpeed = minAutopanSpeed; + } + + /** + * Set the maxAutoPan speed in pixels per second. + * + * @param maxAutopanSpeed number of pixels to assign as the maximum the + * autopan feature can pan the view + */ + public void setMaxAutopanSpeed(final double maxAutopanSpeed) { + this.maxAutopanSpeed = maxAutopanSpeed; + } + + /** + * Returns the minAutoPan speed in pixels per second. + * + * @since 1.3 + * @return minimum distance the autopan feature can pan the view + */ + public double getMinAutoPanSpeed() { + return minAutopanSpeed; + } + + /** + * Returns the maxAutoPan speed in pixels per second. + * + * @since 1.3 + * @return max distance the autopan feature can pan the view by + */ + public double getMaxAutoPanSpeed() { + return maxAutopanSpeed; + } + + /** + * Performs auto-panning if enabled, even when the mouse is not moving. + * + * @param event current drag relevant details about the drag activity + */ + protected void dragActivityStep(final PInputEvent event) { + if (!autopan) { + return; + } + + final PCamera c = event.getCamera(); + final PBounds b = c.getBoundsReference(); + final Point2D l = event.getPositionRelativeTo(c); + final int outcode = b.outcode(l); + final PDimension delta = new PDimension(); + + if ((outcode & Rectangle2D.OUT_TOP) != 0) { + delta.height = validatePanningSpeed(-1.0 - 0.5 * Math.abs(l.getY() - b.getY())); + } + else if ((outcode & Rectangle2D.OUT_BOTTOM) != 0) { + delta.height = validatePanningSpeed(1.0 + 0.5 * Math.abs(l.getY() - (b.getY() + b.getHeight()))); + } + + if ((outcode & Rectangle2D.OUT_RIGHT) != 0) { + delta.width = validatePanningSpeed(1.0 + 0.5 * Math.abs(l.getX() - (b.getX() + b.getWidth()))); + } + else if ((outcode & Rectangle2D.OUT_LEFT) != 0) { + delta.width = validatePanningSpeed(-1.0 - 0.5 * Math.abs(l.getX() - b.getX())); + } + + c.localToView(delta); + + if (delta.width != 0 || delta.height != 0) { + c.translateView(delta.width, delta.height); + } + } + + /** + * Clips the panning speed to the minimum and maximum auto-pan speeds + * assigned. If delta is below the threshold, it will be increased. If + * above, it will be decreased. + * + * @param delta auto-pan delta to be clipped + * @return clipped delta value. + */ + protected double validatePanningSpeed(final double delta) { + final double stepsPerSecond = 1000d / getDragActivity().getStepRate(); + final double minDelta = minAutopanSpeed / stepsPerSecond; + final double maxDelta = maxAutopanSpeed / stepsPerSecond; + + final double absDelta = Math.abs(delta); + + final double clippedDelta; + if (absDelta < minDelta) { + clippedDelta = minDelta; + } + else if (absDelta > maxDelta) { + clippedDelta = maxDelta; + } + else { + clippedDelta = delta; + } + + if (delta < 0) { + return -clippedDelta; + } + else { + return clippedDelta; + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/event/PZoomEventHandler.java b/src/main/java/edu/umd/cs/piccolo/event/PZoomEventHandler.java new file mode 100644 index 0000000..6dfe31c --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/event/PZoomEventHandler.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.event; + +import java.awt.event.InputEvent; +import java.awt.geom.Point2D; + +import edu.umd.cs.piccolo.PCamera; + +/** + * ZoomEventhandler provides event handlers for basic zooming of the + * canvas view with the right (third) button. The interaction is that the + * initial mouse press defines the zoom anchor point, and then moving the mouse + * to the right zooms with a speed proportional to the amount the mouse is moved + * to the right of the anchor point. Similarly, if the mouse is moved to the + * left, the the view is zoomed out. + *
+ * On a Mac with its single mouse button one may wish to change the standard + * right mouse button zooming behavior. This can be easily done with the + * PInputEventFilter. For example to zoom with button one and shift you would do + * this: + *
+ *
+ *
+ *
+ * zoomEventHandler.getEventFilter().setAndMask(InputEvent.BUTTON1_MASK |
+ * InputEvent.SHIFT_MASK);
+ *
+ *
+ *
+ * @version 1.0
+ * @author Jesse Grosjean
+ */
+public class PZoomEventHandler extends PDragSequenceEventHandler {
+
+ /**
+ * A constant used to adjust how sensitive the zooming will be to mouse
+ * movement. The larger the number, the more each delta pixel will affect zooming.
+ */
+ private static final double ZOOM_SENSITIVITY = 0.001;
+ private double minScale = 0;
+ private double maxScale = Double.MAX_VALUE;
+ private Point2D viewZoomPoint;
+
+ /**
+ * Creates a new zoom handler.
+ */
+ public PZoomEventHandler() {
+ super();
+ setEventFilter(new PInputEventFilter(InputEvent.BUTTON3_MASK));
+ }
+
+ // ****************************************************************
+ // Zooming
+ // ****************************************************************
+
+ /**
+ * Returns the minimum view magnification factor that this event handler is
+ * bound by. The default is 0.
+ *
+ * @return the minimum camera view scale
+ */
+ public double getMinScale() {
+ return minScale;
+ }
+
+ /**
+ * Sets the minimum view magnification factor that this event handler is
+ * bound by. The camera is left at its current scale even if
+ * minScale
is larger than the current scale.
+ *
+ * @param minScale the minimum scale, must not be negative.
+ */
+ public void setMinScale(final double minScale) {
+ this.minScale = minScale;
+ }
+
+ /**
+ * Returns the maximum view magnification factor that this event handler is
+ * bound by. The default is Double.MAX_VALUE.
+ *
+ * @return the maximum camera view scale
+ */
+ public double getMaxScale() {
+ return maxScale;
+ }
+
+ /**
+ * Sets the maximum view magnification factor that this event handler is
+ * bound by. The camera is left at its current scale even if
+ * maxScale
is smaller than the current scale. Use
+ * Double.MAX_VALUE to specify the largest possible scale.
+ *
+ * @param maxScale the maximum scale, must not be negative.
+ */
+ public void setMaxScale(final double maxScale) {
+ this.maxScale = maxScale;
+ }
+
+ /**
+ * Records the start point of the zoom. Used when calculating the delta for
+ * zoom speed.
+ *
+ * @param event event responsible for starting the zoom interaction
+ */
+ protected void dragActivityFirstStep(final PInputEvent event) {
+ viewZoomPoint = event.getPosition();
+ super.dragActivityFirstStep(event);
+ }
+
+ /**
+ * Updates the current zoom periodically, regardless of whether the mouse
+ * has moved recently.
+ *
+ * @param event contains information about the current state of the mouse
+ */
+ protected void dragActivityStep(final PInputEvent event) {
+ final PCamera camera = event.getCamera();
+ final double dx = event.getCanvasPosition().getX() - getMousePressedCanvasPoint().getX();
+ double scaleDelta = 1.0 + ZOOM_SENSITIVITY * dx;
+
+ final double currentScale = camera.getViewScale();
+ final double newScale = currentScale * scaleDelta;
+
+ if (newScale < minScale) {
+ scaleDelta = minScale / currentScale;
+ }
+ if (maxScale > 0 && newScale > maxScale) {
+ scaleDelta = maxScale / currentScale;
+ }
+
+ camera.scaleViewAboutPoint(scaleDelta, viewZoomPoint.getX(), viewZoomPoint.getY());
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolo/event/package.html b/src/main/java/edu/umd/cs/piccolo/event/package.html
new file mode 100644
index 0000000..9fa7ad1
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/event/package.html
@@ -0,0 +1,5 @@
+
"SansSerif"
.
+ */
+ // public static final Font DEFAULT_FONT = new Font(Font.SANS_SERIF,
+ // Font.PLAIN, 12); jdk 1.6+
+ public static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 12);
+
+ /**
+ * Default text color if not otherwise specified in the HTML text,
+ * Color.BLACK
.
+ */
+ public static final Color DEFAULT_TEXT_COLOR = Color.BLACK;
+
+ /**
+ * The property name that identifies a change of this node's font (see
+ * {@link #getFont getFont}). Both old and new value will be set in any
+ * property change event.
+ */
+ public static final String PROPERTY_FONT = "font";
+
+ /**
+ * The property code that identifies a change of this node's font (see
+ * {@link #getFont getFont}). Both old and new value will be set in any
+ * property change event.
+ */
+ public static final int PROPERTY_CODE_FONT = 1 << 20;
+
+ /**
+ * The property name that identifies a change of this node's HTML text (see
+ * {@link #getText getText}). Both old and new value will be set in any
+ * property change event.
+ */
+ public static final String PROPERTY_TEXT = "text";
+
+ /**
+ * The property code that identifies a change of this node's HTML text (see
+ * {@link #getText getText}). Both old and new value will be set in any
+ * property change event.
+ */
+ public static final int PROPERTY_CODE_TEXT = 1 << 21;
+
+ /**
+ * The property name that identifies a change of this node's HTML text color
+ * (see {@link #getTextColor getTextColor}). Both old and new value will be set
+ * in any property change event.
+ */
+ public static final String PROPERTY_TEXT_COLOR = "text color";
+
+ /**
+ * The property code that identifies a change of this node's HTML text color
+ * (see {@link #getTextColor getTextColor}). Both old and new value will be set
+ * in any property change event.
+ */
+ public static final int PROPERTY_CODE_TEXT_COLOR = 1 << 22;
+
+ /** Underlying JLabel used to handle the rendering logic. */
+ private final JLabel label;
+
+ /** Object that encapsulates the HTML rendering logic. */
+ private transient View htmlView;
+
+ /**
+ * Create an empty HTML text node with the default font and text color.
+ */
+ public PHtmlView() {
+ this(null, DEFAULT_FONT, DEFAULT_TEXT_COLOR);
+ }
+
+ /**
+ * Create a HTML text node with the specified HTML text and the default font
+ * and text color.
+ *
+ * @param text HTML text for this HTML text node
+ */
+ public PHtmlView(final String text) {
+ this(text, DEFAULT_FONT, DEFAULT_TEXT_COLOR);
+ }
+
+ /**
+ * Create a HTML text node with the specified HTML text, font, and text
+ * color. The font and text color are used to render the HTML text if not
+ * otherwise specified via CSS.
+ *
+ * @param text HTML text for this HTML text node
+ * @param font font for this HTML text node
+ * @param textColor text color for this HTML text node
+ */
+ public PHtmlView(final String text, final Font font, final Color textColor) {
+ label = new JLabel(text);
+ label.setFont(font);
+ label.setForeground(textColor);
+ super.setBounds(0, 0, label.getPreferredSize().getWidth(), label.getPreferredSize().getHeight());
+ update();
+ }
+
+ /**
+ * Return the HTML text for this HTML text node.
+ *
+ * @return the HTML text for this HTML text node
+ */
+ public String getText() {
+ return label.getText();
+ }
+
+ /**
+ * Set the HTML text for this HTML text node to text
.
+ *
+ * + * This is a bound property. + *
+ * + * @param text HTML text for this HTML text node + */ + public void setText(final String text) { + final String oldText = label.getText(); + + if (oldText == null && text == null) { + return; + } + + if (oldText == null || !oldText.equals(text)) { + label.setText(text); + + update(); + firePropertyChange(PROPERTY_CODE_TEXT, PROPERTY_TEXT, oldText, label.getText()); + } + } + + /** + * Return the font for this HTML text node. This font is used to render the + * HTML text if not otherwise specified via CSS. Defaults to + * {@link #DEFAULT_FONT}. + * + * @return the font for this HTML text node + */ + public Font getFont() { + return label.getFont(); + } + + /** + * Set the font for this HTML text node tofont
. This font is
+ * used to render the HTML text if not otherwise specified via CSS.
+ *
+ * + * This is a bound property. + *
+ * + * @param font font for this HTML text node + */ + public void setFont(final Font font) { + final Font oldFont = label.getFont(); + label.setFont(font); + update(); + firePropertyChange(PROPERTY_CODE_FONT, PROPERTY_FONT, oldFont, label.getFont()); + } + + /** + * Return the text color for this HTML text node. This text color is used to + * render the HTML text if not otherwise specified via CSS. Defaults to + * {@link #DEFAULT_TEXT_COLOR}. + * + * @return the text color for this HTML text node + */ + public Color getTextColor() { + return label.getForeground(); + } + + /** + * Set the text color for this HTML text node totextColor
.
+ * This text color is used to render the HTML text if not otherwise
+ * specified via CSS.
+ *
+ * This is a bound property.
+ *
+ * @param textColor text color for this HTML text node
+ */
+ public void setTextColor(final Color textColor) {
+ final Color oldColor = label.getForeground();
+ label.setForeground(textColor);
+ repaint();
+ firePropertyChange(PROPERTY_CODE_TEXT_COLOR, PROPERTY_TEXT_COLOR, oldColor, label.getForeground());
+ }
+
+ /**
+ * Applies all properties to the underlying JLabel, creates an htmlView and
+ * updates bounds.
+ */
+ private void update() {
+ String htmlContent = label.getText();
+ if (htmlContent == null) {
+ htmlContent = "";
+ }
+
+ htmlView = BasicHTML.createHTMLView(label, htmlContent);
+ fitHeightToHtmlContent();
+
+ repaint();
+ }
+
+ /**
+ * Resizes the height to be as tall as its rendered html. Takes wrapping
+ * into account.
+ */
+ private void fitHeightToHtmlContent() {
+ if (getWidth() > 0) {
+ htmlView.setSize((float) getWidth(), 0f);
+
+ float wrapHeight = htmlView.getPreferredSpan(View.Y_AXIS);
+ label.setSize(new Dimension((int) getWidth(), (int) wrapHeight));
+
+ if (getHeight() < wrapHeight) {
+ System.out.println(getHeight());
+ System.out.println(wrapHeight);
+ super.setBounds(getX(), getY(), getWidth(), wrapHeight);
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ public boolean setBounds(final double x, final double y, final double width, final double height) {
+ final boolean boundsChanged = super.setBounds(x, y, width, height);
+ update();
+ return boundsChanged;
+ }
+
+ /** {@inheritDoc} */
+ public boolean setBounds(final Rectangle2D newBounds) {
+ final boolean boundsChanged = super.setBounds(newBounds);
+ update();
+ return boundsChanged;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * + * The HTML text is painted last, so it will appear on top of any child + * nodes. + *
+ */ + protected void paint(final PPaintContext paintContext) { + super.paint(paintContext); + paintContext.pushClip(getBounds()); + final Graphics2D g2 = paintContext.getGraphics(); + htmlView.paint(g2, getBounds().getBounds()); + paintContext.popClip(getBounds()); + } + + /** + * Return the address specified in the HTML link at the specified point in + * this node's local coordinate system, if any. + * + * @param point point in this node's local coordinate system + * @return the address specified in the HTML link at the specified point in + * this node's local coordinate system, ornull
if no
+ * such HTML link exists
+ */
+ public String getLinkAddressAt(final Point2D point) {
+ return getLinkAddressAt(point.getX(), point.getY());
+ }
+
+ /**
+ * Return the address specified in the HTML link at the specified x and y
+ * coordinates in this node's local coordinate system, if any.
+ *
+ * @param x x coordinate in this node's local coordinate system
+ * @param y y coordinate in this node's local coordinate system
+ * @return the address specified in the HTML link at the specified x and y
+ * coordinates in this node's local coordinate system, or
+ * null
if no such HTML link exists
+ */
+ public String getLinkAddressAt(final double x, final double y) {
+ int position = pointToModelIndex(x, y);
+
+ final String text = label.getText();
+
+ String address = null;
+
+ int currentPos = 0;
+ while (currentPos < text.length()) {
+ currentPos = text.indexOf('<', currentPos);
+ if (currentPos == -1 || position < currentPos) {
+ break;
+ }
+
+ final int tagStart = currentPos;
+ final int tagEnd = findTagEnd(text, currentPos);
+
+ if (tagEnd == -1) {
+ return null;
+ }
+
+ currentPos = tagEnd + 1;
+
+ final String tag = text.substring(tagStart, currentPos);
+
+ position += tag.length();
+
+ if ("".equals(tag)) {
+ address = null;
+ }
+ else if (tag.startsWith("-1 if the end of the string was encountered
+ * before the end of the tag was encountered.
+ *
+ * @param text HTML text being searched
+ * @param startPos where in the string to start searching for ">"
+ * @return index after the ">" character
+ */
+ private int findTagEnd(final String text, final int startPos) {
+ int currentPos = startPos;
+
+ currentPos++;
+
+ while (currentPos > 0 && currentPos < text.length() && text.charAt(currentPos) != '>') {
+ if (text.charAt(currentPos) == '\"') {
+ currentPos = text.indexOf('\"', currentPos + 1);
+ }
+ else if (text.charAt(currentPos) == '\'') {
+ currentPos = text.indexOf('\'', currentPos + 1);
+ }
+ currentPos++;
+ }
+
+ if (currentPos == 0 || currentPos >= text.length()) {
+ return -1;
+ }
+
+ return currentPos + 1;
+ }
+
+ /**
+ * Given a tag, extracts the value of the href attribute or returns null if
+ * none was found.
+ *
+ * @param tag from which to extract the href value
+ * @return href value without quotes or null
if not found
+ */
+ private String extractHref(final String tag) {
+ int currentPos = 0;
+
+ final String href = null;
+
+ while (currentPos >= 0 && currentPos < tag.length() - 1) {
+ currentPos = tag.indexOf('=', currentPos + 1);
+ if (currentPos != -1 && isHrefAttributeAssignment(tag, currentPos)) {
+ return extractHrefValue(tag, currentPos + 1);
+ }
+ }
+ return href;
+ }
+
+ /**
+ * Starting at the character after the equal sign of an href=..., it extract
+ * the value. Handles single, double, and no quotes.
+ *
+ * @param tag tag
+ * @param startPos start position
+ * @return value of href or null if not found.
+ */
+ private String extractHrefValue(final String tag, final int startPos) {
+ int currentPos = startPos;
+
+ if (tag.charAt(currentPos) == '\"') {
+ final int startHref = currentPos + 1;
+ currentPos = tag.indexOf('\"', startHref);
+ if (currentPos == -1) {
+ return null;
+ }
+ return tag.substring(startHref, currentPos);
+ }
+ else if (currentPos < tag.length() && tag.charAt(currentPos) == '\'') {
+ final int startHref = currentPos + 1;
+ currentPos = tag.indexOf('\'', startHref);
+ if (currentPos == -1) {
+ return null;
+ }
+ return tag.substring(startHref, currentPos);
+ }
+ else {
+ final int startHref = currentPos;
+
+ if (currentPos < tag.length()) {
+ do {
+ currentPos++;
+ } while (currentPos < tag.length() && tag.charAt(currentPos) != ' ' && tag.charAt(currentPos) != '>');
+ }
+ return tag.substring(startHref, currentPos);
+ }
+ }
+
+ /**
+ * Given the position in a string returns whether it points to the equal
+ * sign of an href attribute.
+ *
+ * @param tag html code of the tag
+ * @param equalPos the index of the assignment
+ * @return true if to left of assignment is href
+ */
+ private boolean isHrefAttributeAssignment(final String tag, final int equalPos) {
+ return tag.charAt(equalPos) == '=' && equalPos > 4 && " href".equals(tag.substring(equalPos - 5, equalPos));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolo/nodes/PImage.java b/src/main/java/edu/umd/cs/piccolo/nodes/PImage.java
new file mode 100644
index 0000000..d6b87fa
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/nodes/PImage.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo.nodes;
+
+import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
+import java.awt.GraphicsEnvironment;
+import java.awt.Image;
+import java.awt.MediaTracker;
+import java.awt.Toolkit;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import javax.imageio.ImageIO;
+import javax.swing.ImageIcon;
+
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.util.PBounds;
+import edu.umd.cs.piccolo.util.PPaintContext;
+
+/**
+ * PImage is a wrapper around a java.awt.Image. If this node is copied or
+ * serialized that image will be converted into a BufferedImage if it is not
+ * already one.
+ *
+ *
+ * @version 1.0
+ * @author Jesse Grosjean
+ */
+public class PImage extends PNode {
+
+ /**
+ * Allows for future serialization code to understand versioned binary
+ * formats.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The property name that identifies a change of this node's image (see
+ * {@link #getImage getImage}). Both old and new value will be set correctly
+ * to Image objects in any property change event.
+ */
+ public static final String PROPERTY_IMAGE = "image";
+ /**
+ * The property code that identifies a change of this node's image (see
+ * {@link #getImage getImage}). Both old and new value will be set correctly
+ * to Image objects in any property change event.
+ */
+
+ public static final int PROPERTY_CODE_IMAGE = 1 << 15;
+
+ private transient Image image;
+
+ /** Constructs a PImage without a java.awt.Image attached. */
+ public PImage() {
+ }
+
+ /**
+ * Construct a new PImage by loading the given fileName and wrapping the
+ * resulting java.awt.Image.
+ *
+ * @param fileName of the image to wrap
+ */
+ public PImage(final String fileName) {
+ this(Toolkit.getDefaultToolkit().getImage(fileName));
+ }
+
+ /**
+ * Construct a new PImage wrapping the given java.awt.Image.
+ *
+ * @param image image that this PImage will wrap
+ */
+ public PImage(final Image image) {
+ setImage(image);
+ }
+
+ /**
+ * Construct a new PImage by loading the given url and wrapping the
+ * resulting java.awt.Image. If the url is null
, create an
+ * empty PImage; this behaviour is useful when fetching resources that may
+ * be missing.
+ *
+ * @param url URL of image resource to load
+ */
+ public PImage(final java.net.URL url) {
+ if (url != null) {
+ setImage(Toolkit.getDefaultToolkit().getImage(url));
+ }
+ }
+
+ /**
+ * Returns the image that is shown by this node, or null if none.
+ *
+ * @return java.awt.Image being wrapped by this node
+ */
+ public Image getImage() {
+ return image;
+ }
+
+ /**
+ * Set the image that is wrapped by this PImage node. This method will also
+ * load the image using a MediaTracker before returning.
+ *
+ * @param fileName file to be wrapped by this PImage
+ */
+ public void setImage(final String fileName) {
+ setImage(Toolkit.getDefaultToolkit().getImage(fileName));
+ }
+
+ /**
+ * Set the image that is wrapped by this PImage node. This method will also
+ * load the image using a MediaTracker before returning.
+ *
+ * @param newImage image to be displayed by this PImage
+ */
+ public void setImage(final Image newImage) {
+ final Image oldImage = image;
+
+ if (newImage == null || newImage instanceof BufferedImage) {
+ image = newImage;
+ }
+ else {
+ image = getLoadedImage(newImage);
+ }
+
+ if (image != null) {
+ setBounds(0, 0, getImage().getWidth(null), getImage().getHeight(null));
+ invalidatePaint();
+ }
+
+ firePropertyChange(PROPERTY_CODE_IMAGE, PROPERTY_IMAGE, oldImage, image);
+ }
+
+ /**
+ * Ensures the image is loaded enough (loading is fine).
+ *
+ * @param newImage to check
+ * @return image or null if not loaded enough.
+ */
+ private Image getLoadedImage(final Image newImage) {
+ final ImageIcon imageLoader = new ImageIcon(newImage);
+ switch (imageLoader.getImageLoadStatus()) {
+ case MediaTracker.LOADING:
+ case MediaTracker.COMPLETE:
+ return imageLoader.getImage();
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Renders the wrapped Image, stretching it appropriately if the bounds of
+ * this PImage doesn't match the bounds of the image.
+ *
+ * @param paintContext context into which the rendering will occur
+ */
+ protected void paint(final PPaintContext paintContext) {
+ if (getImage() == null) {
+ return;
+ }
+
+ final double iw = image.getWidth(null);
+ final double ih = image.getHeight(null);
+
+ final PBounds b = getBoundsReference();
+ final Graphics2D g2 = paintContext.getGraphics();
+
+ if (b.x != 0 || b.y != 0 || b.width != iw || b.height != ih) {
+ g2.translate(b.x, b.y);
+ g2.scale(b.width / iw, b.height / ih);
+ g2.drawImage(image, 0, 0, null);
+ g2.scale(iw / b.width, ih / b.height);
+ g2.translate(-b.x, -b.y);
+ }
+ else {
+ g2.drawImage(image, 0, 0, null);
+ }
+
+ }
+
+ /**
+ * Serializes this PImage to the stream provided. The java.awt.Image wrapped
+ * by this PImage is converted into a BufferedImage when serialized.
+ *
+ * @param out stream into which serialized object will be serialized
+ * @throws IOException if error occurs while writing to the output stream
+ */
+ private void writeObject(final ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ final BufferedImage bufferedImage = toBufferedImage(image, false);
+ if (bufferedImage != null) {
+ ImageIO.write(bufferedImage, "png", out);
+ }
+ }
+
+ /**
+ * Deserializes a PImage from the input stream provided.
+ *
+ * @param in stream from which the PImage should be read
+ * @throws IOException if problem occurs while reading from input stream
+ * @throws ClassNotFoundException occurs is no mapping from the bytes in the
+ * stream can be found to classes available
+ */
+ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ image = ImageIO.read(in);
+ }
+
+ /**
+ * Converts the provided image into a BufferedImage. If alwaysCreateCopy is
+ * false then if the image is already a buffered image it will not be copied
+ * and instead the original image will just be returned.
+ *
+ * @param image the image to be converted
+ * @param alwaysCreateCopy if true, will create a copy even if image is
+ * already a BufferedImage
+ * @return a BufferedImage equivalent to the Image provided
+ */
+ public static BufferedImage toBufferedImage(final Image image, final boolean alwaysCreateCopy) {
+ if (image == null) {
+ return null;
+ }
+
+ if (!alwaysCreateCopy && image instanceof BufferedImage) {
+ return (BufferedImage) image;
+ }
+
+ BufferedImage result;
+
+ if (GraphicsEnvironment.isHeadless()) {
+ result = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
+ }
+ else {
+ final GraphicsConfiguration graphicsConfiguration = GraphicsEnvironment.getLocalGraphicsEnvironment()
+ .getDefaultScreenDevice().getDefaultConfiguration();
+ result = graphicsConfiguration.createCompatibleImage(image.getWidth(null), image.getHeight(null));
+ }
+
+ final Graphics2D g2 = result.createGraphics();
+ g2.drawImage(image, 0, 0, null);
+ g2.dispose();
+ return result;
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolo/nodes/PPath.java b/src/main/java/edu/umd/cs/piccolo/nodes/PPath.java
new file mode 100644
index 0000000..28dfcb5
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/nodes/PPath.java
@@ -0,0 +1,695 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo.nodes;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Shape;
+import java.awt.Stroke;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.RoundRectangle2D;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.activities.PColorActivity;
+import edu.umd.cs.piccolo.activities.PInterpolatingActivity;
+import edu.umd.cs.piccolo.util.PAffineTransform;
+import edu.umd.cs.piccolo.util.PPaintContext;
+import edu.umd.cs.piccolo.util.PUtil;
+
+/**
+ * PPath is a wrapper around a java.awt.geom.GeneralPath. The setBounds
+ * method works by scaling the path to fit into the specified bounds. This
+ * normally works well, but if the specified base bounds get too small then it
+ * is impossible to expand the path shape again since all its numbers have
+ * tended to zero, so application code may need to take this into consideration.
+ *
+ * One option that applications have is to call startResizeBounds
+ * before starting an interaction that may make the bounds very small, and
+ * calling endResizeBounds
when this interaction is finished. When
+ * this is done PPath will use a copy of the original path to do the resizing so
+ * the numbers in the path wont loose resolution.
+ *
+ * This class also provides methods for constructing common shapes using a + * general path. + *
+ *
+ * @version 1.0
+ * @author Jesse Grosjean
+ */
+public class PPath extends PNode {
+
+ /**
+ * Allows for future serialization code to understand versioned binary
+ * formats.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The property name that identifies a change of this node's stroke paint
+ * (see {@link #getStrokePaint getStrokePaint}). Both old and new value will
+ * be set correctly to Paint objects in any property change event.
+ */
+ public static final String PROPERTY_STROKE_PAINT = "strokePaint";
+
+ /**
+ * The property code that identifies a change of this node's stroke paint
+ * (see {@link #getStrokePaint getStrokePaint}). Both old and new value will
+ * be set correctly to Paint objects in any property change event.
+ */
+ public static final int PROPERTY_CODE_STROKE_PAINT = 1 << 16;
+
+ /**
+ * The property name that identifies a change of this node's stroke (see
+ * {@link #getStroke getStroke}). Both old and new value will be set
+ * correctly to Stroke objects in any property change event.
+ */
+ public static final String PROPERTY_STROKE = "stroke";
+
+ /**
+ * The property code that identifies a change of this node's stroke (see
+ * {@link #getStroke getStroke}). Both old and new value will be set
+ * correctly to Stroke objects in any property change event.
+ */
+ public static final int PROPERTY_CODE_STROKE = 1 << 17;
+
+ /**
+ * The property name that identifies a change of this node's path (see
+ * {@link #getPathReference getPathReference}). In any property change event
+ * the new value will be a reference to this node's path, but old value will
+ * always be null.
+ */
+ public static final String PROPERTY_PATH = "path";
+
+ /**
+ * The property code that identifies a change of this node's path (see
+ * {@link #getPathReference getPathReference}). In any property change event
+ * the new value will be a reference to this node's path, but old value will
+ * always be null.
+ */
+ public static final int PROPERTY_CODE_PATH = 1 << 18;
+
+ private static final Rectangle2D.Float TEMP_RECTANGLE = new Rectangle2D.Float();
+ private static final RoundRectangle2D.Float TEMP_ROUNDRECTANGLE = new RoundRectangle2D.Float();
+ private static final Ellipse2D.Float TEMP_ELLIPSE = new Ellipse2D.Float();
+ private static final PAffineTransform TEMP_TRANSFORM = new PAffineTransform();
+ private static final BasicStroke DEFAULT_STROKE = new BasicStroke(1.0f);
+ private static final Color DEFAULT_STROKE_PAINT = Color.black;
+
+ private transient GeneralPath path;
+ private transient GeneralPath resizePath;
+ private transient Stroke stroke;
+ private transient boolean updatingBoundsFromPath;
+ private Paint strokePaint;
+
+ /**
+ * Creates a PPath object in the shape of a rectangle.
+ *
+ * @param x left of the rectangle
+ * @param y top of the rectangle
+ * @param width width of the rectangle
+ * @param height height of the rectangle
+ *
+ * @return created rectangle
+ */
+ public static PPath createRectangle(final float x, final float y, final float width, final float height) {
+ TEMP_RECTANGLE.setFrame(x, y, width, height);
+ final PPath result = new PPath(TEMP_RECTANGLE);
+ result.setPaint(Color.white);
+ return result;
+ }
+
+ /**
+ * Creates a PPath object in the shape of a rounded rectangle.
+ *
+ * @param x left of the rectangle
+ * @param y top of the rectangle
+ * @param width width of the rectangle
+ * @param height height of the rectangle
+ * @param arcWidth the arc width at the corners of the rectangle
+ * @param arcHeight the arc height at the corners of the rectangle
+ *
+ * @return created rounded rectangle
+ */
+ public static PPath createRoundRectangle(final float x, final float y, final float width, final float height,
+ final float arcWidth, final float arcHeight) {
+ TEMP_ROUNDRECTANGLE.setRoundRect(x, y, width, height, arcWidth, arcHeight);
+ final PPath result = new PPath(TEMP_ROUNDRECTANGLE);
+ result.setPaint(Color.white);
+ return result;
+ }
+
+ /**
+ * Creates a PPath object in the shape of an ellipse.
+ *
+ * @param x left of the ellipse
+ * @param y top of the ellipse
+ * @param width width of the ellipse
+ * @param height height of the ellipse
+ *
+ * @return created ellipse
+ */
+ public static PPath createEllipse(final float x, final float y, final float width, final float height) {
+ TEMP_ELLIPSE.setFrame(x, y, width, height);
+ final PPath result = new PPath(TEMP_ELLIPSE);
+ result.setPaint(Color.white);
+ return result;
+ }
+
+ /**
+ * Creates a PPath in the shape of a line.
+ *
+ * @param x1 x component of the first point
+ * @param y1 y component of the first point
+ * @param x2 x component of the second point
+ * @param y2 y component of the second point
+ *
+ * @return created line
+ */
+ public static PPath createLine(final float x1, final float y1, final float x2, final float y2) {
+ final PPath result = new PPath();
+ result.moveTo(x1, y1);
+ result.lineTo(x2, y2);
+ result.setPaint(Color.white);
+ return result;
+ }
+
+ /**
+ * Creates a PPath for the poly-line for the given points.
+ *
+ * @param points array of points for the point lines
+ *
+ * @return created poly-line for the given points
+ */
+ public static PPath createPolyline(final Point2D[] points) {
+ final PPath result = new PPath();
+ result.setPathToPolyline(points);
+ result.setPaint(Color.white);
+ return result;
+ }
+
+ /**
+ * Creates a PPath for the poly-line for the given points.
+ *
+ * @param xp array of x components of the points of the poly-lines
+ * @param yp array of y components of the points of the poly-lines
+ *
+ * @return created poly-line for the given points
+ */
+ public static PPath createPolyline(final float[] xp, final float[] yp) {
+ final PPath result = new PPath();
+ result.setPathToPolyline(xp, yp);
+ result.setPaint(Color.white);
+ return result;
+ }
+
+ /**
+ * Creates an empty PPath with the default paint and stroke.
+ */
+ public PPath() {
+ strokePaint = DEFAULT_STROKE_PAINT;
+ stroke = DEFAULT_STROKE;
+ path = new GeneralPath();
+ }
+
+ /**
+ * Creates an PPath in the given shape with the default paint and stroke.
+ *
+ * @param aShape the desired shape
+ */
+ public PPath(final Shape aShape) {
+ this(aShape, DEFAULT_STROKE);
+ }
+
+ /**
+ * Construct this path with the given shape and stroke. This method may be
+ * used to optimize the creation of a large number of PPaths. Normally
+ * PPaths have a default stroke of width one, but when a path has a non null
+ * stroke it takes significantly longer to compute its bounds. This method
+ * allows you to override that default stroke before the bounds are ever
+ * calculated, so if you pass in a null stroke here you won't ever have to
+ * pay that bounds calculation price if you don't need to.
+ *
+ * @param aShape desired shape or null if you desire an empty path
+ * @param aStroke desired stroke
+ */
+ public PPath(final Shape aShape, final Stroke aStroke) {
+ this();
+ stroke = aStroke;
+ if (aShape != null) {
+ append(aShape, false);
+ }
+ }
+
+ /**
+ * Returns the stroke paint of the PPath.
+ *
+ * @return stroke paint of the PPath
+ */
+ public Paint getStrokePaint() {
+ return strokePaint;
+ }
+
+ /**
+ * Sets the stroke paint of the path.
+ *
+ * @param newStrokePaint the paint to use as this path's stroke paint
+ */
+ public void setStrokePaint(final Paint newStrokePaint) {
+ final Paint oldStrokePaint = strokePaint;
+ strokePaint = newStrokePaint;
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_STROKE_PAINT, PROPERTY_STROKE_PAINT, oldStrokePaint, strokePaint);
+ }
+
+ /**
+ * Returns the stroke to use when drawing the path.
+ *
+ * @return current stroke of path
+ */
+ public Stroke getStroke() {
+ return stroke;
+ }
+
+ /**
+ * Sets the stroke to use when drawing the path.
+ *
+ * @param aStroke stroke to use when drawing the path
+ */
+ public void setStroke(final Stroke aStroke) {
+ final Stroke old = stroke;
+ stroke = aStroke;
+ updateBoundsFromPath();
+ invalidatePaint();
+ firePropertyChange(PROPERTY_CODE_STROKE, PROPERTY_STROKE, old, stroke);
+ }
+
+ /** Stores the original size of the path before resizing started. */
+ public void startResizeBounds() {
+ resizePath = new GeneralPath(path);
+ }
+
+ /** Clears the size of the path before resizing. */
+ public void endResizeBounds() {
+ resizePath = null;
+ }
+
+ /**
+ * Set the bounds of this path. This method works by scaling the path to fit
+ * into the specified bounds. This normally works well, but if the specified
+ * base bounds get too small then it is impossible to expand the path shape
+ * again since all its numbers have tended to zero, so application code may
+ * need to take this into consideration.
+ *
+ * @param x new left position of bounds
+ * @param y new top position of bounds
+ * @param width the new width of the bounds
+ * @param height the new height of the bounds
+ */
+ protected void internalUpdateBounds(final double x, final double y, final double width, final double height) {
+ if (updatingBoundsFromPath || path == null) {
+ return;
+ }
+
+ if (resizePath != null) {
+ path.reset();
+ path.append(resizePath, false);
+ }
+
+ final Rectangle2D pathBounds = path.getBounds2D();
+ final Rectangle2D pathStrokeBounds = getPathBoundsWithStroke();
+ final double strokeOutset = Math.max(pathStrokeBounds.getWidth() - pathBounds.getWidth(), pathStrokeBounds
+ .getHeight()
+ - pathBounds.getHeight());
+
+ double adjustedX = x + strokeOutset / 2;
+ double adjustedY = y + strokeOutset / 2;
+ double adjustedWidth = width - strokeOutset;
+ double adjustedHeight = height - strokeOutset;
+
+ final double scaleX;
+ if (adjustedWidth == 0 || pathBounds.getWidth() == 0) {
+ scaleX = 1;
+ }
+ else {
+ scaleX = adjustedWidth / pathBounds.getWidth();
+ }
+
+ final double scaleY;
+ if (adjustedHeight == 0 || pathBounds.getHeight() == 0) {
+ scaleY = 1;
+ }
+ else {
+ scaleY = adjustedHeight / pathBounds.getHeight();
+ }
+
+ TEMP_TRANSFORM.setToIdentity();
+ TEMP_TRANSFORM.translate(adjustedX, adjustedY);
+ TEMP_TRANSFORM.scale(scaleX, scaleY);
+ TEMP_TRANSFORM.translate(-pathBounds.getX(), -pathBounds.getY());
+
+ path.transform(TEMP_TRANSFORM);
+ }
+
+ /**
+ * Returns true if path crosses the provided bounds. Takes visibility of
+ * path into account.
+ *
+ * @param aBounds bounds being tested for intersection
+ * @return true if path visibly crosses bounds
+ */
+ public boolean intersects(final Rectangle2D aBounds) {
+ if (super.intersects(aBounds)) {
+ if (getPaint() != null && path.intersects(aBounds)) {
+ return true;
+ }
+ else if (stroke != null && strokePaint != null) {
+ return stroke.createStrokedShape(path).intersects(aBounds);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Calculates the path's bounds taking stroke into account.
+ *
+ * @return bounds of the path taking stroke width into account
+ */
+ public Rectangle2D getPathBoundsWithStroke() {
+ if (stroke != null) {
+ return stroke.createStrokedShape(path).getBounds2D();
+ }
+ else {
+ return path.getBounds2D();
+ }
+ }
+
+ /**
+ * Recomputes the bounds taking stroke into account.
+ */
+ public void updateBoundsFromPath() {
+ updatingBoundsFromPath = true;
+ if (path == null) {
+ resetBounds();
+ }
+ else {
+ final Rectangle2D b = getPathBoundsWithStroke();
+ setBounds(b.getX(), b.getY(), b.getWidth(), b.getHeight());
+ }
+ updatingBoundsFromPath = false;
+ }
+
+ /**
+ * Paints the path in the provided paintContext. Can perform very
+ * differently depending on whether the path is being drawn using its stroke
+ * or its paint.
+ *
+ * It both are provided to the path, fun ensues.
+ *
+ * @param paintContext context in which painting is occurring
+ */
+ protected void paint(final PPaintContext paintContext) {
+ final Paint p = getPaint();
+ final Graphics2D g2 = paintContext.getGraphics();
+
+ if (p != null) {
+ g2.setPaint(p);
+ g2.fill(path);
+ }
+
+ if (stroke != null && strokePaint != null) {
+ g2.setPaint(strokePaint);
+ g2.setStroke(stroke);
+ g2.draw(path);
+ }
+ }
+
+ /**
+ * Provides direct access to the underlying GeneralPath object.
+ *
+ * @return underlying GeneralPath
+ */
+ public GeneralPath getPathReference() {
+ return path;
+ }
+
+ /**
+ * Appends a "move" operation to the end of the path.
+ *
+ * @param x the x component of the point to move to
+ * @param y the y component of the point to move to
+ */
+ public void moveTo(final float x, final float y) {
+ path.moveTo(x, y);
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Draws a line from the last point in the path to point provided.
+ *
+ * @param x the x component of the point
+ * @param y the y component of the point
+ */
+ public void lineTo(final float x, final float y) {
+ path.lineTo(x, y);
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Adds a curved segment, defined by two new points, to the path by drawing
+ * a Quadratic curve that intersects both the current coordinates and the
+ * coordinates (x2, y2), using the specified point (x1, y1) as a quadratic
+ * parametric control point.
+ *
+ * @param x1 x component of quadratic parametric control point
+ * @param y1 y component of quadratic parametric control point
+ * @param x2 x component of point through which quad curve will pass
+ * @param y2 y component of point through which quad curve will pass
+ */
+ public void quadTo(final float x1, final float y1, final float x2, final float y2) {
+ path.quadTo(x1, y1, x2, y2);
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Adds a curved segment, defined by three new points, to the path by
+ * drawing a Bézier curve that intersects both the current coordinates and
+ * the coordinates (x3, y3), using the specified points (x1, y1) and (x2,
+ * y2) as Bézier control points.
+ *
+ * @param x1 x component of first Bézier control point
+ * @param y1 y component of first Bézier control point
+ * @param x2 x component of second Bézier control point
+ * @param y2 y component of second Bézier control point
+ * @param x3 x component of point through which curve must pass
+ * @param y3 y component of point through which curve must pass
+ */
+ public void curveTo(final float x1, final float y1, final float x2, final float y2,
+ final float x3, final float y3) {
+ path.curveTo(x1, y1, x2, y2, x3, y3);
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Appends the provided shape to the end of this path, it may conditionally
+ * connect them together if they are disjoint.
+ *
+ * @param aShape shape to append
+ * @param connect whether to perform a lineTo operation to the beginning of
+ * the shape before appending
+ */
+ public void append(final Shape aShape, final boolean connect) {
+ path.append(aShape, connect);
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Replaces this PPath's path with the one provided.
+ *
+ * @param aShape shape to replace the current one with
+ */
+ public void setPathTo(final Shape aShape) {
+ path.reset();
+ append(aShape, false);
+ }
+
+ /**
+ * Resets the path to a rectangle with the dimensions and position provided.
+ *
+ * @param x left of the rectangle
+ * @param y top of te rectangle
+ * @param width width of the rectangle
+ * @param height height of the rectangle
+ */
+ public void setPathToRectangle(final float x, final float y, final float width, final float height) {
+ TEMP_RECTANGLE.setFrame(x, y, width, height);
+ setPathTo(TEMP_RECTANGLE);
+ }
+
+ /**
+ * Resets the path to an ellipse positioned at the coordinate provided with
+ * the dimensions provided.
+ *
+ * @param x left of the ellipse
+ * @param y top of the ellipse
+ * @param width width of the ellipse
+ * @param height height of the ellipse
+ */
+ public void setPathToEllipse(final float x, final float y, final float width, final float height) {
+ TEMP_ELLIPSE.setFrame(x, y, width, height);
+ setPathTo(TEMP_ELLIPSE);
+ }
+
+ /**
+ * Sets the path to a sequence of segments described by the points.
+ *
+ * @param points points to that lie along the generated path
+ */
+ public void setPathToPolyline(final Point2D[] points) {
+ path.reset();
+ path.moveTo((float) points[0].getX(), (float) points[0].getY());
+ for (int i = 1; i < points.length; i++) {
+ path.lineTo((float) points[i].getX(), (float) points[i].getY());
+ }
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Sets the path to a sequence of segments described by the point components
+ * provided.
+ *
+ * @param xp the x components of the points along the path
+ * @param yp the y components of the points along the path
+ */
+ public void setPathToPolyline(final float[] xp, final float[] yp) {
+ path.reset();
+ path.moveTo(xp[0], yp[0]);
+ for (int i = 1; i < xp.length; i++) {
+ path.lineTo(xp[i], yp[i]);
+ }
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Marks the path as closed. Making changes to it impossible.
+ */
+ public void closePath() {
+ path.closePath();
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Empties the path.
+ */
+ public void reset() {
+ path.reset();
+ firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path);
+ updateBoundsFromPath();
+ invalidatePaint();
+ }
+
+ /**
+ * Writes this PPath object to the output stream provided. Necessary since
+ * stroke and path are not serializable by default.
+ *
+ * @param out output stream into which objects are to be serialized
+ * @throws IOException if serialiazing to output stream fails
+ */
+ private void writeObject(final ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ PUtil.writeStroke(stroke, out);
+ PUtil.writePath(path, out);
+ }
+
+ /**
+ * Deserializes a PPath object from the provided input stream. This method
+ * is required since Strokes and GeneralPaths are not serializable by
+ * default.
+ *
+ * @param in stream from which to read this PPath's state
+ * @throws IOException when exception occurs reading from input stream
+ * @throws ClassNotFoundException
+ */
+ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ stroke = PUtil.readStroke(in);
+ path = PUtil.readPath(in);
+ }
+
+ /**
+ * added by miura 2012/11/29
+ * @param destColor
+ * @param duration
+ * @return
+ */
+ public PInterpolatingActivity animateToStrokeColor(final Color destColor, final long duration) {
+ if (duration == 0) {
+ setPaint(destColor);
+ return null;
+ }
+ else {
+ final PColorActivity.Target t = new PColorActivity.Target() {
+ public Color getColor() {
+ return (Color) getStrokePaint();
+ }
+
+ public void setColor(final Color color) {
+ setStrokePaint(color);
+ }
+ };
+
+ final PColorActivity ca = new PColorActivity(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, t, destColor);
+ addActivity(ca);
+ return ca;
+ }
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolo/nodes/PText.java b/src/main/java/edu/umd/cs/piccolo/nodes/PText.java
new file mode 100644
index 0000000..5863963
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/nodes/PText.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo.nodes;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.font.LineBreakMeasurer;
+import java.awt.font.TextAttribute;
+import java.awt.font.TextLayout;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.util.ArrayList;
+
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.util.PPaintContext;
+
+/**
+ * PText is a multi-line text node. The text will flow to base on the
+ * width of the node's bounds.
+ *
+ * @version 1.1
+ * @author Jesse Grosjean
+ */
+public class PText extends PNode {
+
+ /**
+ * Allows for future serialization code to understand versioned binary
+ * formats.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The property name that identifies a change of this node's text (see
+ * {@link #getText getText}). Both old and new value will be set in any
+ * property change event.
+ */
+ public static final String PROPERTY_TEXT = "text";
+
+ /**
+ * The property code that identifies a change of this node's text (see
+ * {@link #getText getText}). Both old and new value will be set in any
+ * property change event.
+ */
+ public static final int PROPERTY_CODE_TEXT = 1 << 19;
+
+ /**
+ * The property name that identifies a change of this node's font (see
+ * {@link #getFont getFont}). Both old and new value will be set in any
+ * property change event.
+ */
+ public static final String PROPERTY_FONT = "font";
+
+ /**
+ * The property code that identifies a change of this node's font (see
+ * {@link #getFont getFont}). Both old and new value will be set in any
+ * property change event.
+ */
+ public static final int PROPERTY_CODE_FONT = 1 << 20;
+
+ /**
+ * The property name that identifies a change of this node's text paint (see
+ * {@link #getTextPaint getTextPaint}). Both old and new value will be set
+ * in any property change event.
+ *
+ * @since 1.3
+ */
+ public static final String PROPERTY_TEXT_PAINT = "text paint";
+
+ /**
+ * The property code that identifies a change of this node's text paint (see
+ * {@link #getTextPaint getTextPaint}). Both old and new value will be set
+ * in any property change event.
+ *
+ * @since 1.3
+ */
+ public static final int PROPERTY_CODE_TEXT_PAINT = 1 << 21;
+
+ /**
+ * Default font, 12 point "SansSerif"
. Will be made final in
+ * version 2.0.
+ */
+ // public static final Font DEFAULT_FONT = new Font(Font.SANS_SERIF,
+ // Font.PLAIN, 12); jdk 1.6+
+ public static Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 12);
+
+ /**
+ * Default greek threshold, 5.5d
. Will be made final in version
+ * 2.0.
+ */
+ public static double DEFAULT_GREEK_THRESHOLD = 5.5d;
+
+ /**
+ * Default horizontal alignment, Component.LEFT_ALIGNMENT
.
+ *
+ * @since 1.3
+ */
+ public static final float DEFAULT_HORIZONTAL_ALIGNMENT = Component.LEFT_ALIGNMENT;
+
+ /**
+ * Default text, ""
.
+ *
+ * @since 1.3
+ */
+ public static final String DEFAULT_TEXT = "";
+
+ /**
+ * Default text paint, Color.BLACK
.
+ *
+ * @since 1.3
+ */
+ public static final Paint DEFAULT_TEXT_PAINT = Color.BLACK;
+
+ /** Empty text layout array. */
+ private static final TextLayout[] EMPTY_TEXT_LAYOUT_ARRAY = new TextLayout[0];
+
+ /** Text for this text node. */
+ private String text = DEFAULT_TEXT;
+
+ /** Text paint for this text node. */
+ private Paint textPaint = DEFAULT_TEXT_PAINT;
+
+ /** Font for this text node. */
+ private Font font = DEFAULT_FONT;
+
+ /**
+ * Greek threshold in screen font size for this text node. Will be made
+ * private in version 2.0.
+ */
+ protected double greekThreshold = DEFAULT_GREEK_THRESHOLD;
+
+ /** Horizontal alignment for this text node. */
+ private float horizontalAlignment = DEFAULT_HORIZONTAL_ALIGNMENT;
+
+ /**
+ * True if this text node should constrain its height to the height of its
+ * text.
+ */
+ private boolean constrainHeightToTextHeight = true;
+
+ /**
+ * True if this text node should constrain its height to the height of its
+ * text.
+ */
+ private boolean constrainWidthToTextWidth = true;
+
+ /** One or more lines of text layout. */
+ private transient TextLayout[] lines;
+
+ /**
+ * Create a new text node with no text (""
).
+ */
+ public PText() {
+ super();
+ setText(DEFAULT_TEXT);
+ }
+
+ /**
+ * Create a new text node with the specified text.
+ *
+ * @param text text for this text node
+ */
+ public PText(final String text) {
+ this();
+ setText(text);
+ }
+
+ /**
+ * @deprecated by {@link #getHorizontalAlignment()}
+ *
+ * @return the horizontal alignment value of this node
+ */
+ public float getJustification() {
+ return getHorizontalAlignment();
+ }
+
+ /**
+ * @deprecated by {@link #setHorizontalAlignment(float)}
+ *
+ * @param justification horizontal alignment value to assign to this node
+ */
+ public void setJustification(final float justification) {
+ setHorizontalAlignment(justification);
+ }
+
+ /**
+ * Return the horizontal alignment for this text node. The horizontal
+ * alignment will be one of Component.LEFT_ALIGNMENT
,
+ * Component.CENTER_ALIGNMENT
, or
+ * Component.RIGHT_ALIGNMENT
. Defaults to
+ * {@link #DEFAULT_HORIZONTAL_ALIGNMENT}.
+ *
+ * @since 1.3
+ * @return the horizontal alignment for this text node
+ */
+ public float getHorizontalAlignment() {
+ return horizontalAlignment;
+ }
+
+ /**
+ * Set the horizontal alignment for this text node to
+ * horizontalAlignment
.
+ *
+ * @since 1.3
+ * @param horizontalAlignment horizontal alignment, must be one of
+ * Component.LEFT_ALIGNMENT
,
+ * Component.CENTER_ALIGNMENT
, or
+ * Component.RIGHT_ALIGNMENT
+ */
+ public void setHorizontalAlignment(final float horizontalAlignment) {
+ if (!validHorizontalAlignment(horizontalAlignment)) {
+ throw new IllegalArgumentException("horizontalAlignment must be one of Component.LEFT_ALIGNMENT, "
+ + "Component.CENTER_ALIGNMENT, or Component.RIGHT_ALIGNMENT");
+ }
+ this.horizontalAlignment = horizontalAlignment;
+ }
+
+ /**
+ * Return true if the specified horizontal alignment is one of
+ * Component.LEFT_ALIGNMENT
,
+ * Component.CENTER_ALIGNMENT
, or
+ * Component.RIGHT_ALIGNMENT
.
+ *
+ * @param horizontalAlignment horizontal alignment
+ * @return true if the specified horizontal alignment is one of
+ * Component.LEFT_ALIGNMENT
,
+ * Component.CENTER_ALIGNMENT
, or
+ * Component.RIGHT_ALIGNMENT
+ */
+ private static boolean validHorizontalAlignment(final float horizontalAlignment) {
+ return Component.LEFT_ALIGNMENT == horizontalAlignment || Component.CENTER_ALIGNMENT == horizontalAlignment
+ || Component.RIGHT_ALIGNMENT == horizontalAlignment;
+ }
+
+ /**
+ * Return the paint used to paint this node's text.
+ *
+ * @return the paint used to paint this node's text
+ */
+ public Paint getTextPaint() {
+ return textPaint;
+ }
+
+ /**
+ * Set the paint used to paint this node's text to textPaint
.
+ *
+ *
+ * This is a bound property. + *
+ * + * @param textPaint text paint + */ + public void setTextPaint(final Paint textPaint) { + if (textPaint == this.textPaint) { + return; + } + final Paint oldTextPaint = this.textPaint; + this.textPaint = textPaint; + invalidatePaint(); + firePropertyChange(PROPERTY_CODE_TEXT_PAINT, PROPERTY_TEXT_PAINT, oldTextPaint, this.textPaint); + } + + /** + * Return true if this text node should constrain its width to the width of + * its text. Defaults totrue
.
+ *
+ * @return true if this text node should constrain its width to the width of
+ * its text
+ */
+ public boolean isConstrainWidthToTextWidth() {
+ return constrainWidthToTextWidth;
+ }
+
+ /**
+ * Set to true
if this text node should constrain its width to
+ * the width of its text.
+ *
+ * @param constrainWidthToTextWidth true if this text node should constrain
+ * its width to the width of its text
+ */
+ public void setConstrainWidthToTextWidth(final boolean constrainWidthToTextWidth) {
+ this.constrainWidthToTextWidth = constrainWidthToTextWidth;
+ recomputeLayout();
+ }
+
+ /**
+ * Return true if this text node should constrain its height to the height
+ * of its text. Defaults to true
.
+ *
+ * @return true if this text node should constrain its height to the height
+ * of its text
+ */
+ public boolean isConstrainHeightToTextHeight() {
+ return constrainHeightToTextHeight;
+ }
+
+ /**
+ * Set to true
if this text node should constrain its height to
+ * the height of its text.
+ *
+ * @param constrainHeightToTextHeight true if this text node should
+ * constrain its height to the width of its text
+ */
+ public void setConstrainHeightToTextHeight(final boolean constrainHeightToTextHeight) {
+ this.constrainHeightToTextHeight = constrainHeightToTextHeight;
+ recomputeLayout();
+ }
+
+ /**
+ * Return the greek threshold in screen font size. When the screen font size
+ * will be below this threshold the text is rendered as 'greek' instead of
+ * drawing the text glyphs. Defaults to {@link #DEFAULT_GREEK_THRESHOLD}.
+ *
+ * @see PText#paintGreek(PPaintContext)
+ * @return the current greek threshold in screen font size
+ */
+ public double getGreekThreshold() {
+ return greekThreshold;
+ }
+
+ /**
+ * Set the greek threshold in screen font size to
+ * greekThreshold
. When the screen font size will be below this
+ * threshold the text is rendered as 'greek' instead of drawing the text
+ * glyphs.
+ *
+ * @see PText#paintGreek(PPaintContext)
+ * @param greekThreshold greek threshold in screen font size
+ */
+ public void setGreekThreshold(final double greekThreshold) {
+ this.greekThreshold = greekThreshold;
+ invalidatePaint();
+ }
+
+ /**
+ * Return the text for this text node. Defaults to {@link #DEFAULT_TEXT}.
+ *
+ * @return the text for this text node
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Set the text for this node to text
. The text will be broken
+ * up into multiple lines based on the size of the text and the bounds width
+ * of this node.
+ *
+ * + * This is a bound property. + *
+ * + * @param newText text for this text node + */ + public void setText(final String newText) { + if (newText == null && text == null || newText != null && newText.equals(text)) { + return; + } + + final String oldText = text; + if (newText == null) { + text = DEFAULT_TEXT; + } + else { + text = newText; + } + lines = null; + recomputeLayout(); + invalidatePaint(); + firePropertyChange(PROPERTY_CODE_TEXT, PROPERTY_TEXT, oldText, text); + } + + /** + * Return the font for this text node. Defaults to {@link #DEFAULT_FONT}. + * + * @return the font for this text node + */ + public Font getFont() { + return font; + } + + /** + * Set the font for this text node tofont
. Note that in
+ * Piccolo if you want to change the size of a text object it's often a
+ * better idea to scale the PText node instead of changing the font size to
+ * get that same effect. Using very large font sizes can slow performance.
+ *
+ * + * This is a bound property. + *
+ * + * @param font font for this text node + */ + public void setFont(final Font font) { + if (font == this.font) { + return; + } + final Font oldFont = this.font; + if (font == null) { + this.font = DEFAULT_FONT; + } + else { + this.font = font; + } + + lines = null; + recomputeLayout(); + invalidatePaint(); + firePropertyChange(PROPERTY_CODE_FONT, PROPERTY_FONT, oldFont, this.font); + } + + /** + * Compute the bounds of the text wrapped by this node. The text layout is + * wrapped based on the bounds of this node. + */ + public void recomputeLayout() { + final ArrayList linesList = new ArrayList(); + double textWidth = 0; + double textHeight = 0; + + if (text != null && text.length() > 0) { + final AttributedString atString = new AttributedString(text); + atString.addAttribute(TextAttribute.FONT, getFont()); + final AttributedCharacterIterator itr = atString.getIterator(); + final LineBreakMeasurer measurer = new LineBreakMeasurer(itr, PPaintContext.RENDER_QUALITY_HIGH_FRC); + final float availableWidth; + if (constrainWidthToTextWidth) { + availableWidth = Float.MAX_VALUE; + } + else { + availableWidth = (float) getWidth(); + } + + int nextLineBreakOffset = text.indexOf('\n'); + if (nextLineBreakOffset == -1) { + nextLineBreakOffset = Integer.MAX_VALUE; + } + else { + nextLineBreakOffset++; + } + + while (measurer.getPosition() < itr.getEndIndex()) { + final TextLayout aTextLayout = computeNextLayout(measurer, availableWidth, nextLineBreakOffset); + + if (nextLineBreakOffset == measurer.getPosition()) { + nextLineBreakOffset = text.indexOf('\n', measurer.getPosition()); + if (nextLineBreakOffset == -1) { + nextLineBreakOffset = Integer.MAX_VALUE; + } + else { + nextLineBreakOffset++; + } + } + + linesList.add(aTextLayout); + textHeight += aTextLayout.getAscent(); + textHeight += aTextLayout.getDescent() + aTextLayout.getLeading(); + textWidth = Math.max(textWidth, aTextLayout.getAdvance()); + } + } + + lines = (TextLayout[]) linesList.toArray(EMPTY_TEXT_LAYOUT_ARRAY); + + if (constrainWidthToTextWidth || constrainHeightToTextHeight) { + double newWidth = getWidth(); + double newHeight = getHeight(); + + if (constrainWidthToTextWidth) { + newWidth = textWidth; + } + + if (constrainHeightToTextHeight) { + newHeight = textHeight; + } + + super.setBounds(getX(), getY(), newWidth, newHeight); + } + } + + /** + * Compute the next layout using the specified line break measurer, + * available width, and next line break offset. + * + * @param lineBreakMeasurer line break measurer + * @param availableWidth available width + * @param nextLineBreakOffset next line break offset + * @return the next layout computed using the specified line break measurer, + * available width, and next line break offset + */ + protected TextLayout computeNextLayout(final LineBreakMeasurer lineBreakMeasurer, final float availableWidth, + final int nextLineBreakOffset) { + return lineBreakMeasurer.nextLayout(availableWidth, nextLineBreakOffset, false); + } + + /** + * Paint greek with the specified paint context. + * + * @since 1.3 + * @param paintContext paint context + */ + protected void paintGreek(final PPaintContext paintContext) { + // empty + } + + /** + * Paint text with the specified paint context. + * + * @since 1.3 + * @param paintContext paint context + */ + protected void paintText(final PPaintContext paintContext) { + final float x = (float) getX(); + float y = (float) getY(); + final float bottomY = (float) getHeight() + y; + + final Graphics2D g2 = paintContext.getGraphics(); + + if (lines == null) { + recomputeLayout(); + repaint(); + return; + } + + g2.setPaint(textPaint); + + for (int i = 0; i < lines.length; i++) { + final TextLayout tl = lines[i]; + y += tl.getAscent(); + + if (bottomY < y) { + return; + } + + final float offset = (float) (getWidth() - tl.getAdvance()) * horizontalAlignment; + + tl.draw(g2, x + offset, y); + + y += tl.getDescent() + tl.getLeading(); + } + } + + /** {@inheritDoc} */ + protected void paint(final PPaintContext paintContext) { + super.paint(paintContext); + if (textPaint == null) { + return; + } + final float screenFontSize = getFont().getSize() * (float) paintContext.getScale(); + if (screenFontSize <= greekThreshold) { + paintGreek(paintContext); + } + paintText(paintContext); + } + + /** {@inheritDoc} */ + protected void internalUpdateBounds(final double x, final double y, final double width, final double height) { + recomputeLayout(); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/nodes/package.html b/src/main/java/edu/umd/cs/piccolo/nodes/package.html new file mode 100644 index 0000000..1039fc0 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/nodes/package.html @@ -0,0 +1,5 @@ + +This package contains nodes that may be useful for Piccolo applications. These +nodes are intended to be useful, but not definitive. Many applications will also +end up defining their nodes which can be used along with these. + diff --git a/src/main/java/edu/umd/cs/piccolo/package.html b/src/main/java/edu/umd/cs/piccolo/package.html new file mode 100644 index 0000000..be1fa0e --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/package.html @@ -0,0 +1,18 @@ + +Piccolo is a general-purpose Java-based engine that supports 2D visualizations. +A primary characteristic of Piccolo is that it is designed to support zoomable +information spaces, although any particular applications may or may not +take advantage of this feature. Piccolo is implemented entirely in Java 2 (1.4), +and as such runs identically on any platform that supports Java 2.+ +Piccolo is not an application in itself, but rather it is an engine that is designed +to support applications that require the ability to create, manipulate, and render +object-oriented graphics. If you are familiar with the terminology of 3D graphics, +Piccolo supports a scenegraph. This is a data structure that represents a hierarchy +of graphical objects. Piccolo uses a tuned run-time system to render the scenegraph +as quickly as possible to support interactive applications.
+
+This is the root package for all Jazz classes. It contains the core scenegraph
+classes itself, and in addition, contains the activities
, event
,
+nodes
and util
packages that are used to build Jazz applications.
+
diff --git a/src/main/java/edu/umd/cs/piccolo/util/PAffineTransform.java b/src/main/java/edu/umd/cs/piccolo/util/PAffineTransform.java
new file mode 100644
index 0000000..860773b
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/util/PAffineTransform.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo.util;
+
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Dimension2D;
+import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * PAffineTransform is a subclass of AffineTransform that has been
+ * extended with convenience methods.
+ *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PAffineTransform extends AffineTransform { + /** + * Allows for future serialization code to understand versioned binary + * formats. + */ + private static final long serialVersionUID = 1L; + + /** Used internally to speed up computation. */ + private static final double[] PTS1 = new double[8]; + + /** Used internally to speed up computation. */ + private static final double[] PTS2 = new double[8]; + + /** + * Constructs a new AffineTransform representing the Identity + * transformation. + */ + public PAffineTransform() { + super(); + } + + /** + * Constructs a new AffineTransform from an array of double precision values + * representing either the 4 non-translation entries or the 6 specifiable + * entries of the 3x3 transformation matrix. The values are retrieved from + * the array as { m00 m10 m01 m11 [m02 m12]}. + * + * @param flatmatrix the double array containing the values to be set in the + * new AffineTransform object. The length of the array is assumed + * to be at least 4. If the length of the array is less than 6, + * only the first 4 values are taken. If the length of the array + * is greater than 6, the first 6 values are taken. + */ + public PAffineTransform(final double[] flatmatrix) { + super(flatmatrix); + } + + /** + * Constructs a new AffineTransform from an array of floating point values + * representing either the 4 non-translation entries or the 6 specifiable + * entries of the 3x3 transformation matrix. The values are retrieved from + * the array as { m00 m10 m01 m11 [m02 m12]}. + * + * @param flatmatrix the float array containing the values to be set in the + * new AffineTransform object. The length of the array is assumed + * to be at least 4. If the length of the array is less than 6, + * only the first 4 values are taken. If the length of the array + * is greater than 6, the first 6 values are taken. + */ + public PAffineTransform(final float[] flatmatrix) { + super(flatmatrix); + } + + /** + * Constructs a new AffineTransform from 6 double precision values + * representing the 6 specifiable entries of the 3x3 transformation matrix. + * + * @param m00 the X coordinate scaling element of the 3x3 matrix + * @param m10 the Y coordinate shearing element of the 3x3 matrix + * @param m01 the X coordinate shearing element of the 3x3 matrix + * @param m11 the Y coordinate scaling element of the 3x3 matrix + * @param m02 the X coordinate translation element of the 3x3 matrix + * @param m12 the Y coordinate translation element of the 3x3 matrix + */ + public PAffineTransform(final double m00, final double m10, final double m01, final double m11, final double m02, + final double m12) { + super(m00, m10, m01, m11, m02, m12); + } + + /** + * Constructs a new AffineTransform from 6 floating point values + * representing the 6 specifiable entries of the 3x3 transformation matrix. + * + * @param m00 the X coordinate scaling element of the 3x3 matrix + * @param m10 the Y coordinate shearing element of the 3x3 matrix + * @param m01 the X coordinate shearing element of the 3x3 matrix + * @param m11 the Y coordinate scaling element of the 3x3 matrix + * @param m02 the X coordinate translation element of the 3x3 matrix + * @param m12 the Y coordinate translation element of the 3x3 matrix + */ + public PAffineTransform(final float m00, final float m10, final float m01, final float m11, final float m02, + final float m12) { + super(m00, m10, m01, m11, m02, m12); + } + + /** + * Constructs a new AffineTransform that is a copy of the specified + * AffineTransform object. + * + * @param tx transform to copy + */ + public PAffineTransform(final AffineTransform tx) { + super(tx); + } + + /** + * Scales the transform about the given point by the given scale. + * + * @param scale to transform the transform by + * @param x x coordinate around which the scale should take place + * @param y y coordinate around which the scale should take place + */ + public void scaleAboutPoint(final double scale, final double x, final double y) { + translate(x, y); + scale(scale, scale); + translate(-x, -y); + } + + /** + * Returns the scale applied to this transform. Note that it does so by + * computing the change in length of a unit segment after being passed + * through the transform. This means that a transform that a transform that + * doesn't scale the in the x but doubles the y will be reported as 2. + * + * @return the different in length of a unit segment after being + * transformed. + */ + public double getScale() { + PTS1[0] = 0; // x1 + PTS1[1] = 0; // y1 + PTS1[2] = 1; // x2 + PTS1[3] = 0; // y2 + transform(PTS1, 0, PTS2, 0, 2); + return Point2D.distance(PTS2[0], PTS2[1], PTS2[2], PTS2[3]); + } + + /** + * Sets the scale about to the origin of this transform to the scale + * provided. + * + * @param scale The desired resulting scale + */ + public void setScale(final double scale) { + if (scale == 0) { + throw new PAffineTransformException("Can't set scale to 0", this); + } + + scaleAboutPoint(scale / getScale(), 0, 0); + } + + /** + * Applies modifies the transform so that it translates by the given offset. + * + * @param tx x translation of resulting transform + * @param ty y translation of resulting transform + */ + public void setOffset(final double tx, final double ty) { + setTransform(getScaleX(), getShearY(), getShearX(), getScaleY(), tx, ty); + } + + /** + * Returns the rotation applied to this affine transform in radians. The + * value returned will be between 0 and 2pi. + * + * @return rotation in radians + */ + public double getRotation() { + PTS1[0] = 0; // x1 + PTS1[1] = 0; // y1 + PTS1[2] = 1; // x2 + PTS1[3] = 0; // y2 + + transform(PTS1, 0, PTS2, 0, 2); + + final double dy = Math.abs(PTS2[3] - PTS2[1]); + final double l = Point2D.distance(PTS2[0], PTS2[1], PTS2[2], PTS2[3]); + double rotation = Math.asin(dy / l); + + // correct for quadrant + if (PTS2[3] - PTS2[1] > 0) { + if (PTS2[2] - PTS2[0] < 0) { + rotation = Math.PI - rotation; + } + } + else if (PTS2[2] - PTS2[0] > 0) { + rotation = 2 * Math.PI - rotation; + } + else { + rotation = rotation + Math.PI; + } + + return rotation; + } + + /** + * Set rotation in radians. This is not cumulative. + * + * @param theta desired rotation in radians. + */ + public void setRotation(final double theta) { + rotate(theta - getRotation()); + } + + /** + * Applies the transform to the provided dimension. + * + * @param dimSrc source dimension + * @param dimDst will be changed to be the transformed dimension, may be + * null + * @return the transformed dimension + */ + public Dimension2D transform(final Dimension2D dimSrc, final Dimension2D dimDst) { + final Dimension2D result; + if (dimDst == null) { + result = (Dimension2D) dimSrc.clone(); + } + else { + result = dimDst; + } + + PTS1[0] = dimSrc.getWidth(); + PTS1[1] = dimSrc.getHeight(); + deltaTransform(PTS1, 0, PTS2, 0, 1); + result.setSize(PTS2[0], PTS2[1]); + return result; + } + + /** + * Applies the inverse of this transform to the source point if possible. + * + * @since 1.3 + * @param ptSrc point to be transformed + * @param ptDst result of transform will be placed in this point + * + * @return the transformed point + */ + public Point2D inverseTransform(final Point2D ptSrc, final Point2D ptDst) { + try { + return super.inverseTransform(ptSrc, ptDst); + } + catch (final NoninvertibleTransformException e) { + throw new PAffineTransformException("Could not invert Transform", e, this); + } + } + + /** + * Applies the inverse of this transform to the source dimension if + * possible. + * + * @param dimSrc dimension to be transformed + * @param dimDst result of transform will be placed in this dimension + * + * @return the transformed dimension + */ + public Dimension2D inverseTransform(final Dimension2D dimSrc, final Dimension2D dimDst) { + final Dimension2D result; + if (dimDst == null) { + result = (Dimension2D) dimSrc.clone(); + } + else { + result = dimDst; + } + + final double width = dimSrc.getWidth(); + final double height = dimSrc.getHeight(); + final double m00 = getScaleX(); + final double m11 = getScaleY(); + final double m01 = getShearX(); + final double m10 = getShearY(); + final double det = m00 * m11 - m01 * m10; + + if (Math.abs(det) > Double.MIN_VALUE) { + result.setSize((width * m11 - height * m01) / det, (height * m00 - width * m10) / det); + } + else { + throw new PAffineTransformException("Could not invert transform", this); + } + + return result; + } + + /** + * Applies this transform to the source rectangle and stores the result in + * rectDst. + * + * @param rectSrc rectangle to be transformed + * @param rectDst result of transform will be placed in this rectangle + * + * @return the transformed rectangle + */ + public Rectangle2D transform(final Rectangle2D rectSrc, final Rectangle2D rectDst) { + final Rectangle2D result; + if (rectDst == null) { + result = (Rectangle2D) rectSrc.clone(); + } + else { + result = rectDst; + } + + if (rectSrc.isEmpty()) { + result.setRect(rectSrc); + if (result instanceof PBounds) { + ((PBounds) result).reset(); + } + return result; + } + + double scale; + + switch (getType()) { + case AffineTransform.TYPE_IDENTITY: + if (rectSrc != result) { + result.setRect(rectSrc); + } + break; + + case AffineTransform.TYPE_TRANSLATION: + result.setRect(rectSrc.getX() + getTranslateX(), rectSrc.getY() + getTranslateY(), rectSrc.getWidth(), + rectSrc.getHeight()); + break; + + case AffineTransform.TYPE_UNIFORM_SCALE: + scale = getScaleX(); + result.setRect(rectSrc.getX() * scale, rectSrc.getY() * scale, rectSrc.getWidth() * scale, rectSrc + .getHeight() + * scale); + break; + + case AffineTransform.TYPE_TRANSLATION | AffineTransform.TYPE_UNIFORM_SCALE: + scale = getScaleX(); + result.setRect(rectSrc.getX() * scale + getTranslateX(), rectSrc.getY() * scale + getTranslateY(), + rectSrc.getWidth() * scale, rectSrc.getHeight() * scale); + break; + + default: + final double[] pts = rectToArray(rectSrc); + transform(pts, 0, pts, 0, 4); + rectFromArray(result, pts); + break; + } + + return result; + } + + /** + * Applies the inverse of this transform to the source rectangle and stores + * the result in rectDst. + * + * @param rectSrc rectangle to be transformed + * @param rectDst result of transform will be placed in this rectangle + * + * @return the transformed rectangle + */ + public Rectangle2D inverseTransform(final Rectangle2D rectSrc, final Rectangle2D rectDst) { + final Rectangle2D result; + if (rectDst == null) { + result = (Rectangle2D) rectSrc.clone(); + } + else { + result = rectDst; + } + + if (rectSrc.isEmpty()) { + result.setRect(rectSrc); + if (result instanceof PBounds) { + ((PBounds) result).reset(); + } + return result; + } + + double scale; + + switch (getType()) { + case AffineTransform.TYPE_IDENTITY: + if (rectSrc != result) { + result.setRect(rectSrc); + } + break; + + case AffineTransform.TYPE_TRANSLATION: + result.setRect(rectSrc.getX() - getTranslateX(), rectSrc.getY() - getTranslateY(), rectSrc.getWidth(), + rectSrc.getHeight()); + break; + + case AffineTransform.TYPE_UNIFORM_SCALE: + scale = getScaleX(); + if (scale == 0) { + throw new PAffineTransformException("Could not invertTransform rectangle", this); + } + + result.setRect(rectSrc.getX() / scale, rectSrc.getY() / scale, rectSrc.getWidth() / scale, rectSrc + .getHeight() + / scale); + break; + + case AffineTransform.TYPE_TRANSLATION | AffineTransform.TYPE_UNIFORM_SCALE: + scale = getScaleX(); + if (scale == 0) { + throw new PAffineTransformException("Could not invertTransform rectangle", this); + } + result.setRect((rectSrc.getX() - getTranslateX()) / scale, (rectSrc.getY() - getTranslateY()) / scale, + rectSrc.getWidth() / scale, rectSrc.getHeight() / scale); + break; + + default: + final double[] pts = rectToArray(rectSrc); + try { + inverseTransform(pts, 0, pts, 0, 4); + } + catch (final NoninvertibleTransformException e) { + throw new PAffineTransformException("Could not invert transform", e, this); + } + rectFromArray(result, pts); + break; + } + + return result; + } + + /** + * Builds an array of coordinates from an source rectangle. + * + * @param aRectangle rectangle from which points coordinates will be + * extracted + * + * @return coordinate array + */ + private static double[] rectToArray(final Rectangle2D aRectangle) { + PTS1[0] = aRectangle.getX(); + PTS1[1] = aRectangle.getY(); + PTS1[2] = PTS1[0] + aRectangle.getWidth(); + PTS1[3] = PTS1[1]; + PTS1[4] = PTS1[0] + aRectangle.getWidth(); + PTS1[5] = PTS1[1] + aRectangle.getHeight(); + PTS1[6] = PTS1[0]; + PTS1[7] = PTS1[1] + aRectangle.getHeight(); + return PTS1; + } + + /** + * Creates a rectangle from an array of coordinates. + * + * @param aRectangle rectangle into which coordinates will be stored + * @param pts coordinate source + */ + private static void rectFromArray(final Rectangle2D aRectangle, final double[] pts) { + double minX = pts[0]; + double minY = pts[1]; + double maxX = pts[0]; + double maxY = pts[1]; + + double x; + double y; + + for (int i = 1; i < 4; i++) { + x = pts[2 * i]; + y = pts[2 * i + 1]; + + if (x < minX) { + minX = x; + } + if (y < minY) { + minY = y; + } + if (x > maxX) { + maxX = x; + } + if (y > maxY) { + maxY = y; + } + } + aRectangle.setRect(minX, minY, maxX - minX, maxY - minY); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PAffineTransformException.java b/src/main/java/edu/umd/cs/piccolo/util/PAffineTransformException.java new file mode 100644 index 0000000..1a11bf5 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PAffineTransformException.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +/** + * This class is used to encapsulate exceptions that may occur while performing transform operations. + * + * @since 1.3 + */ +public class PAffineTransformException extends RuntimeException { + /** + * Allows for future serialization code to understand versioned binary + * formats. + */ + private static final long serialVersionUID = 1L; + + private final PAffineTransform errantTransform; + + /** + * Constructs an Exception that represents an error with the + * errantTransform. + * + * @param errantTransform transform that caused the error + */ + public PAffineTransformException(final PAffineTransform errantTransform) { + this.errantTransform = errantTransform; + } + + /** + * Constructs an Exception that represents an error with the + * errantTransform. + * + * @param message Text message provided by the programmer about the context + * of the error + * @param errantTransform transform that caused the error + */ + public PAffineTransformException(final String message, final PAffineTransform errantTransform) { + super(message); + this.errantTransform = errantTransform; + } + + /** + * Constructs an Exception that wraps another and records the errant + * transform. + * + * @param throwable the root cause of the exception + * @param errantTransform transform that's related to the error + */ + public PAffineTransformException(final Throwable throwable, final PAffineTransform errantTransform) { + super(throwable); + this.errantTransform = errantTransform; + } + + /** + * Constructs an Exception that wraps another and records the errant + * transform and provides a human readable message about the exception's + * context. + * + * @param message Text message provided by the programmer about the context + * of the error + * @param throwable the root cause of the exception + * @param errantTransform transform that's related to the error + */ + public PAffineTransformException(final String message, final Throwable throwable, + final PAffineTransform errantTransform) { + super(message, throwable); + this.errantTransform = errantTransform; + } + + /** + * Used to access the transform related to this exception. + * + * @return transform related to the exception + */ + public PAffineTransform getErrantTransform() { + return errantTransform; + } + +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PBounds.java b/src/main/java/edu/umd/cs/piccolo/util/PBounds.java new file mode 100644 index 0000000..f417732 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PBounds.java @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +import java.awt.geom.Dimension2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * PBounds is simply a Rectangle2D.Double with extra methods that more + * properly deal with the case when the rectangle is "empty". A PBounds has an + * extra bit to store emptiness. In this state, adding new geometry replaces the + * current geometry. A PBounds is emptied with the reset() method. A useful side + * effect of the reset method is that it only modifies the fIsEmpty variable, + * the other x, y, with, height variables are left alone. This is used by + * Piccolo's layout management system to see if a the full bounds of a node has + * really changed when it is recomputed. See PNode.validateLayout. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PBounds extends Rectangle2D.Double implements Serializable { + /** + * Allows for future serialization code to understand versioned binary + * formats. + */ + private static final long serialVersionUID = 1L; + + private boolean isEmpty = true; + + /** + * Creates an empty bounds. + */ + public PBounds() { + super(); + } + + /** + * Creates a bounds identical to the one provided. + * + * @param aBounds bounds to be copied + */ + public PBounds(final PBounds aBounds) { + this(aBounds.x, aBounds.y, aBounds.width, aBounds.height); + isEmpty = aBounds.isEmpty(); + } + + /** + * Creates a bounds with the same shape as the rectangle provided. + * + * @param aBounds rectangle to be copied + */ + public PBounds(final Rectangle2D aBounds) { + this(aBounds.getX(), aBounds.getY(), aBounds.getWidth(), aBounds.getHeight()); + isEmpty = aBounds.isEmpty(); + } + + /** + * Constructs a PBounds object with the given center point and the specified + * insets. + * + * @param aCenterPoint resulting center point of the PBounds object + * @param insetX distance from left and right the center should be + * @param insetY distance from top and bottom the center should be + */ + public PBounds(final Point2D aCenterPoint, final double insetX, final double insetY) { + this(aCenterPoint.getX(), aCenterPoint.getY(), 0, 0); + inset(insetX, insetY); + } + + /** + * Constructs a PBounds object at the given coordinates with the given + * dimensions. + * + * @param x left of bounds + * @param y top of bounds + * @param width width of bounds + * @param height height of bounds + */ + public PBounds(final double x, final double y, final double width, final double height) { + super(x, y, width, height); + isEmpty = false; + } + + /** + * Returns a clone of this node. + * + * @return cloned copy of this bounds + */ + public Object clone() { + return new PBounds(this); + } + + /** + * Returns true if this bounds has been flagged as empty. Not necessarily if + * it is empty. + * + * @return true if bounds marked as empty + */ + public boolean isEmpty() { + return isEmpty; + } + + /** + * Flags this bounds as empty. + * + * @return itself for chaining + */ + public PBounds reset() { + isEmpty = true; + return this; + } + + /** + * Resets the bounds to (0,0,0,0) and flags it as empty. + * + * @return itself for chaining + */ + public PBounds resetToZero() { + x = 0; + y = 0; + width = 0; + height = 0; + isEmpty = true; + return this; + } + + /** + * Sets the bounds to the same shape as the rectangle. And flags the bounds + * as not empty. + * + * @param r rectangle to copy + */ + public void setRect(final Rectangle2D r) { + super.setRect(r); + isEmpty = false; + } + + /** + * Sets the bounds to the same shape as the bounds provided. And flags the + * bounds as not empty. + * + * @param b bounds to copy + */ + public void setRect(final PBounds b) { + isEmpty = b.isEmpty; + x = b.x; + y = b.y; + width = b.width; + height = b.height; + } + + /** + * Sets the shape of the bounds to the position and dimension provided. + * + * @param x new left of bounds + * @param y new top of bounds + * @param width new width of bounds + * @param height new height of bounds + */ + public void setRect(final double x, final double y, final double width, final double height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + isEmpty = false; + } + + /** + * Grows the bounds to contain the coordinate provided. + * + * @param newx x component of point + * @param newy y component of point + */ + public void add(final double newx, final double newy) { + if (isEmpty) { + setRect(newx, newy, 0, 0); + isEmpty = false; + } + else { + super.add(newx, newy); + } + } + + /** + * Grows bounds to contain the rectangle if needed. + * + * @param r rectangle being added + */ + public void add(final Rectangle2D r) { + if (isEmpty) { + setRect(r); + } + else { + super.add(r); + } + } + + /** + * Changes this bounds to contain the provided bounds. + * + * @param bounds bounds being added + */ + public void add(final PBounds bounds) { + if (bounds.isEmpty) { + return; + } + else if (isEmpty) { + x = bounds.x; + y = bounds.y; + width = bounds.width; + height = bounds.height; + isEmpty = false; + } + else { + final double x1 = Math.min(x, bounds.x); + final double y1 = Math.min(y, bounds.y); + final double x2 = Math.max(x + width, bounds.x + bounds.width); + final double y2 = Math.max(y + height, bounds.y + bounds.height); + + x = x1; + y = y1; + width = x2 - x1; + height = y2 - y1; + isEmpty = false; + } + } + + /** + * Returns the x,y coordinate of the bounds. + * + * @return coordinate of the bounds + */ + public Point2D getOrigin() { + return new Point2D.Double(x, y); + } + + /** + * Changes the origin of these bounds. And flags it as non-empty. + * + * @param x new x component of bounds + * @param y new y component of the bounds + * @return the modified PBounds with its new origin + */ + public PBounds setOrigin(final double x, final double y) { + this.x = x; + this.y = y; + isEmpty = false; + return this; + } + + /** + * Returns the size of the bounds. + * + * @return size of the bounds + */ + public Dimension2D getSize() { + return new PDimension(width, height); + } + + /** + * Changes the size of the bounds, but retains the origin. + * + * @param width new width of the bounds + * @param height new height of the bounds + */ + public void setSize(final double width, final double height) { + setRect(x, y, width, height); + } + + /** + * Returns the midpoint of the bounds. + * + * @return midpoint of the bounds + */ + public Point2D getCenter2D() { + return new Point2D.Double(getCenterX(), getCenterY()); + } + + /** + * Translates the bounds by the given deltas. + * + * @param dx amount to move x + * @param dy amount to move y + * @return itself for chaining + */ + public PBounds moveBy(final double dx, final double dy) { + setOrigin(x + dx, y + dy); + return this; + } + + /** + * Rounds the rectangle to the next largest bounds who's measurements are + * integers. Note: this is not the same as rounding its measurements. + */ + public void expandNearestIntegerDimensions() { + x = Math.floor(x); + y = Math.floor(y); + width = Math.ceil(width); + height = Math.ceil(height); + } + + /** + * Adjust the measurements of this bounds so that they are the amounts given + * "in" from their previous border. + * + * @param dx amount to move in from border along horizontal axis + * @param dy amount to move in from border along vertical axis + * @return itself for chaining + */ + public PBounds inset(final double dx, final double dy) { + setRect(x + dx, y + dy, width - dx * 2, height - dy * 2); + return this; + } + + /** + * Returns the required translation in order for this bounds origin to sit + * on the center of the provided rectangle. + * + * @param targetBounds rectangle to measure the center of + * @return the delta required to move to center of the targetBounds + */ + public PDimension deltaRequiredToCenter(final Rectangle2D targetBounds) { + final PDimension result = new PDimension(); + final double xDelta = getCenterX() - targetBounds.getCenterX(); + final double yDelta = getCenterY() - targetBounds.getCenterY(); + result.setSize(xDelta, yDelta); + return result; + } + + /** + * Returns the required translation in order for these to contain the bounds + * provided. + * + * @param targetBounds rectangle to measure the center of + * @return the delta required in order for the bounds to overlap completely + * the targetBounds + */ + public PDimension deltaRequiredToContain(final Rectangle2D targetBounds) { + final PDimension result = new PDimension(); + + if (contains(targetBounds)) { + return result; + } + + final double targetMaxX = targetBounds.getMaxX(); + final double targetMinX = targetBounds.getMinX(); + final double targetMaxY = targetBounds.getMaxY(); + final double targetMinY = targetBounds.getMinY(); + final double maxX = getMaxX(); + final double minX = getMinX(); + final double maxY = getMaxY(); + final double minY = getMinY(); + + if (targetMaxX > maxX ^ targetMinX < minX) { + final double difMaxX = targetMaxX - maxX; + final double difMinX = targetMinX - minX; + if (Math.abs(difMaxX) < Math.abs(difMinX)) { + result.width = difMaxX; + } + else { + result.width = difMinX; + } + } + + if (targetMaxY > maxY ^ targetMinY < minY) { + final double difMaxY = targetMaxY - maxY; + final double difMinY = targetMinY - minY; + if (Math.abs(difMaxY) < Math.abs(difMinY)) { + result.height = difMaxY; + } + else { + result.height = difMinY; + } + } + + return result; + } + + private void writeObject(final ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + out.writeDouble(x); + out.writeDouble(y); + out.writeDouble(width); + out.writeDouble(height); + } + + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + x = in.readDouble(); + y = in.readDouble(); + width = in.readDouble(); + height = in.readDouble(); + } + + /** + * Returns a string representation of this PBounds for debugging purposes. + * + * @return string representation of this PBounds + */ + public String toString() { + final StringBuffer result = new StringBuffer(); + + result.append(getClass().getName().replaceAll(".*\\.", "")); + result.append('['); + + if (isEmpty) { + result.append("EMPTY"); + } + else { + result.append("x="); + result.append(x); + result.append(",y="); + result.append(y); + result.append(",width="); + result.append(width); + result.append(",height="); + result.append(height); + } + + result.append(']'); + + return result.toString(); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PDebug.java b/src/main/java/edu/umd/cs/piccolo/util/PDebug.java new file mode 100644 index 0000000..22f1380 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PDebug.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; + +import javax.swing.SwingUtilities; + +/** + * PDebug is used to set framework wide debugging flags. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PDebug { + /** Set to true to display clip bounds boxes. */ + public static boolean debugRegionManagement = false; + + /** + * Set to true if you want to display common errors with painting and + * threading. + */ + public static boolean debugPaintCalls = false; + + /** Set to true to display frame rate in the console. */ + public static boolean debugPrintFrameRate = false; + + /** Set to true to display used memory in console. */ + public static boolean debugPrintUsedMemory = false; + + /** Displays bounding boxes around nodes. Used in PCamera. */ + public static boolean debugBounds = false; + + /** Displays a tint to all shapes within a bounding box. */ + public static boolean debugFullBounds = false; + + /** Whether to complain whenever common threading issues occur. */ + public static boolean debugThreads = false; + + /** How often in frames result info should be printed to the console. */ + public static int printResultsFrameRate = 10; + + private static int debugPaintColor; + private static long framesProcessed; + private static long startProcessingOutputTime; + private static long startProcessingInputTime; + private static long processOutputTime; + private static long processInputTime; + private static boolean processingOutput; + + private PDebug() { + super(); + } + + /** + * Generates a color for use while debugging. + * + * @return a color for use while debugging. + */ + public static Color getDebugPaintColor() { + final int color = 100 + debugPaintColor++ % 10 * 10; + return new Color(color, color, color, 150); + } + + /** + * Checks that process inputs is being doing from the Swing Dispatch Thread. + */ + public static void scheduleProcessInputs() { + if (debugThreads && !SwingUtilities.isEventDispatchThread()) { + System.out.println("scene graph manipulated on wrong thread"); + } + } + + /** + * Ensures that painting is not invalidating paint regions and that it's + * being called from the dispatch thread. + */ + public static void processRepaint() { + if (processingOutput && debugPaintCalls) { + System.err + .println("Got repaint while painting scene. This can result in a recursive process that degrades performance."); + } + + if (debugThreads && !SwingUtilities.isEventDispatchThread()) { + System.out.println("repaint called on wrong thread"); + } + } + + /** + * Returns whether output is being processed. + * + * @return whether output is being processed + */ + public static boolean getProcessingOutput() { + return processingOutput; + } + + /** + * Records that processing of ouptut has begun. + */ + public static void startProcessingOutput() { + processingOutput = true; + startProcessingOutputTime = System.currentTimeMillis(); + } + + /** + * Flags processing of output as finished. Updates all stats in the process. + * + * @param g graphics context in which processing has finished + */ + public static void endProcessingOutput(final Graphics g) { + processOutputTime += System.currentTimeMillis() - startProcessingOutputTime; + framesProcessed++; + + if (framesProcessed % printResultsFrameRate == 0) { + if (PDebug.debugPrintFrameRate) { + System.out.println("Process output frame rate: " + getOutputFPS() + " fps"); + System.out.println("Process input frame rate: " + getInputFPS() + " fps"); + System.out.println("Total frame rate: " + getTotalFPS() + " fps"); + System.out.println(); + resetFPSTiming(); + } + + if (PDebug.debugPrintUsedMemory) { + System.out.println("Approximate used memory: " + getApproximateUsedMemory() / 1024 + " k"); + } + } + + if (PDebug.debugRegionManagement) { + final Graphics2D g2 = (Graphics2D) g; + g2.setColor(PDebug.getDebugPaintColor()); + g2.fill(g.getClipBounds().getBounds2D()); + } + + processingOutput = false; + } + + /** + * Records that processing of input has started. + */ + public static void startProcessingInput() { + startProcessingInputTime = System.currentTimeMillis(); + } + + /** + * Records that processing of input has finished. + */ + public static void endProcessingInput() { + processInputTime += System.currentTimeMillis() - startProcessingInputTime; + } + + /** + * Return how many frames are processed and painted per second. Note that + * since piccolo doesn't paint continuously this rate will be slow unless + * you are interacting with the system or have activities scheduled. + * + * @return frame rate achieved + */ + public static double getTotalFPS() { + if (framesProcessed > 0) { + return 1000.0 / ((processInputTime + processOutputTime) / (double) framesProcessed); + } + else { + return 0; + } + } + + /** + * Return the frames per second used to process input events and activities. + * + * @return # of frames per second that were allocated to processing input + */ + public static double getInputFPS() { + if (processInputTime > 0 && framesProcessed > 0) { + return 1000.0 / (processInputTime / (double) framesProcessed); + } + else { + return 0; + } + } + + /** + * Return the frames per seconds used to paint graphics to the screen. + * + * @return # of frames per second that were used up to processing output + */ + public static double getOutputFPS() { + if (processOutputTime > 0 && framesProcessed > 0) { + return 1000.0 / (processOutputTime / (double) framesProcessed); + } + else { + return 0; + } + } + + /** + * Return the number of frames that have been processed since the last time + * resetFPSTiming was called. + * + * @return total number of frames processed + */ + public long getFramesProcessed() { + return framesProcessed; + } + + /** + * Reset the variables used to track FPS. If you reset seldom they you will + * get good average FPS values, if you reset more often only the frames + * recorded after the last reset will be taken into consideration. + */ + public static void resetFPSTiming() { + framesProcessed = 0; + processInputTime = 0; + processOutputTime = 0; + } + + /** + * Returns an approximation of the amount of memory that is being used. + * + * Not that this call might affecting timings. + * + * @return approximate # of bytes of memory used + */ + public static long getApproximateUsedMemory() { + System.gc(); + System.runFinalization(); + final long totalMemory = Runtime.getRuntime().totalMemory(); + final long free = Runtime.getRuntime().freeMemory(); + return totalMemory - free; + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PDimension.java b/src/main/java/edu/umd/cs/piccolo/util/PDimension.java new file mode 100644 index 0000000..ad55eaf --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PDimension.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +import java.awt.geom.Dimension2D; +import java.awt.geom.Point2D; +import java.io.Serializable; + +/** + * PDimension this class should be removed once a concrete Dimension2D + * that supports doubles is added to java. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PDimension extends Dimension2D implements Serializable { + /** + * Allows for future serialization code to understand versioned binary + * formats. + */ + private static final long serialVersionUID = 1L; + + /** The width of the dimension. */ + public double width; + + /** The height of the dimension. */ + public double height; + + /** + * Returns a dimension with no width or height. + */ + public PDimension() { + super(); + } + + /** + * Copies the provided dimension. + * + * @param aDimension dimension to copy + */ + public PDimension(final Dimension2D aDimension) { + this(aDimension.getWidth(), aDimension.getHeight()); + } + + /** + * Creates a dimension with the provided dimensions. + * + * @param aWidth desired width + * @param aHeight desired height + */ + public PDimension(final double aWidth, final double aHeight) { + super(); + width = aWidth; + height = aHeight; + } + + /** + * Creates a dimension that's the size of a rectangel with the points + * provided as opposite corners. + * + * @param p1 first point on rectangle + * @param p2 point diagonally across from p1 + */ + public PDimension(final Point2D p1, final Point2D p2) { + width = p2.getX() - p1.getX(); + height = p2.getY() - p1.getY(); + } + + /** + * Returns the height of the dimension. + * + * @return height height of the dimension + */ + public double getHeight() { + return height; + } + + /** + * Returns the width of the dimension. + * + * @return width width of the dimension + */ + public double getWidth() { + return width; + } + + /** + * Resizes the dimension to have the dimensions provided. + * + * @param aWidth desired width + * @param aHeight desired height + */ + public void setSize(final double aWidth, final double aHeight) { + width = aWidth; + height = aHeight; + } + + /** + * Returns a string representation of this dimension object. + * + * @return string representation of this dimension object. + */ + public String toString() { + final StringBuffer result = new StringBuffer(); + + result.append(super.toString().replaceAll(".*\\.", "")); + result.append('['); + result.append("width="); + result.append(width); + result.append(",height="); + result.append(height); + result.append(']'); + + return result.toString(); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PNodeFilter.java b/src/main/java/edu/umd/cs/piccolo/util/PNodeFilter.java new file mode 100644 index 0000000..72f1e2f --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PNodeFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +import edu.umd.cs.piccolo.PNode; + +/** + * PNodeFilter is a interface that filters (accepts or rejects) nodes. + * Its main use is to retrieve all the children of a node the meet some criteria + * by using the method PNode.getAllNodes(collection, filter); + *
+ *
+ * @version 1.0
+ * @author Jesse Grosjean
+ */
+public interface PNodeFilter {
+
+ /**
+ * Return true if the filter should accept the given node.
+ *
+ * @param aNode node under test
+ * @return true if node should be accepted
+ */
+ boolean accept(PNode aNode);
+
+ /**
+ * Return true if the filter should test the children of the given node for
+ * acceptance.
+ *
+ * @param aNode parent being tested
+ * @return true if children should be tested for acceptance
+ */
+ boolean acceptChildrenOf(PNode aNode);
+}
diff --git a/src/main/java/edu/umd/cs/piccolo/util/PObjectOutputStream.java b/src/main/java/edu/umd/cs/piccolo/util/PObjectOutputStream.java
new file mode 100644
index 0000000..d924747
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolo/util/PObjectOutputStream.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolo.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+
+/**
+ * PObjectOutputStream is an extension of ObjectOutputStream to handle
+ * optional elements. This is similar to the concept of Java's
+ * "weak references", but applied to object serialization rather than garbage
+ * collection. Here, PObjectOutputStream provides a method,
+ * writeConditionalObject
, which only serializes the specified
+ * object to the stream if there is a strong reference (if it has been written
+ * somewhere else using writeObject()) to that object elsewhere in the stream.
+ *
+ * To discover strong references to objects, PObjectOutputStream uses a
+ * two-phase writing process. First, a "discovery" phase is used to find out
+ * what objects are about to be serialized. This works by effectively
+ * serializing the object graph to /dev/null, recording which objects are
+ * unconditionally written using the standard writeObject method. Then, in the
+ * second "write" phase, ObjectOutputStream actually serializes the data to the
+ * output stream. During this phase, calls to writeConditionalObject() will only
+ * write the specified object if the object was found to be serialized during
+ * the discovery stage. If the object was not recorded during the discovery
+ * stage, a an optional null (the default) is unconditionally written in place
+ * of the object. To skip writting out the null use
+ * writeConditionalObject(object, false)
+ *
+ * By careful implementation of readObject and writeObject methods, streams + * serialized using PObjectOutputStream can be deserialized using the standard + * ObjectInputStream. + *
+ * + * @version 1.0 + * @author Jon Meyer + * @author Jesse Grosjean + */ +public class PObjectOutputStream extends ObjectOutputStream { + + private boolean writingRoot; + private final HashMap unconditionallyWritten; + + /** + * Transform the given object into an array of bytes. + * + * @param object the object to be transformed + * @return array of bytes representing the given object + * @throws IOException when serialization system throws one + */ + public static byte[] toByteArray(final Object object) throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final PObjectOutputStream zout = new PObjectOutputStream(out); + zout.writeObjectTree(object); + return out.toByteArray(); + } + + /** + * Constructs a PObjectOutputStream that wraps the provided OutputStream. + * + * @param out underlying outputstream that will receive the serialized + * objects + * + * @throws IOException when underlying subsystem throws one + */ + public PObjectOutputStream(final OutputStream out) throws IOException { + super(out); + unconditionallyWritten = new HashMap(); + } + + /** + * Writes the provided object to the underlying stream like an ordination + * ObjectOutputStream except that it does not record duplicates at all. + * + * @param object object to be serialized + * + * @throws IOException when underlying subsystem throws one + */ + public void writeObjectTree(final Object object) throws IOException { + writingRoot = true; + recordUnconditionallyWritten(object); // record pass + writeObject(object); // write pass + writingRoot = false; + } + + /** + * Writes the given object, but only if it was not in the object tree + * multiple times. + * + * @param object object to write to the stream. + * @throws IOException when underlying subsystem throws one + */ + public void writeConditionalObject(final Object object) throws IOException { + if (!writingRoot) { + throw new RuntimeException( + "writeConditionalObject() may only be called when a root object has been written."); + } + + if (unconditionallyWritten.containsKey(object)) { + writeObject(object); + } + else { + writeObject(null); + } + } + + /** + * Resets the ObjectOutputStream clearing any memory about objects already + * being written while it's at it. + * + * @throws IOException when underlying subsystem throws one + */ + public void reset() throws IOException { + super.reset(); + unconditionallyWritten.clear(); + } + + /** + * Performs a scan of objects that can be serialized once. + * + * @param aRoot Object from which to start the scan + * @throws IOException when serialization fails + */ + protected void recordUnconditionallyWritten(final Object aRoot) throws IOException { + class ZMarkObjectOutputStream extends PObjectOutputStream { + public ZMarkObjectOutputStream() throws IOException { + super(NULL_OUTPUT_STREAM); + enableReplaceObject(true); + } + + public Object replaceObject(final Object object) { + unconditionallyWritten.put(object, Boolean.TRUE); + return object; + } + + public void writeConditionalObject(final Object object) throws IOException { + } + } + new ZMarkObjectOutputStream().writeObject(aRoot); + } + + private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { + public void close() { + } + + public void flush() { + } + + public void write(final byte[] b) { + } + + public void write(final byte[] b, final int off, final int len) { + } + + public void write(final int b) { + } + }; +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PPaintContext.java b/src/main/java/edu/umd/cs/piccolo/util/PPaintContext.java new file mode 100644 index 0000000..56861cf --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PPaintContext.java @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import edu.umd.cs.piccolo.PCamera; + +/** + * PPaintContext is used by piccolo nodes to paint themselves on the + * screen. PPaintContext wraps a Graphics2D to implement painting. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PPaintContext { + /** Used for lowering quality of rendering when requested. */ + public static final int LOW_QUALITY_RENDERING = 0; + + /** Used for improving quality of rendering when requested. */ + public static final int HIGH_QUALITY_RENDERING = 1; + + /** Font context to use while in low quality rendering. */ + public static FontRenderContext RENDER_QUALITY_LOW_FRC = new FontRenderContext(null, false, true); + + /** Font context to use while in high quality rendering. */ + public static FontRenderContext RENDER_QUALITY_HIGH_FRC = new FontRenderContext(null, true, true); + + /** + * @deprecated will disappear as soon as possible Global for accessing the + * current paint context while painting. + **/ + public static PPaintContext CURRENT_PAINT_CONTEXT; + + /** Used while calculating scale at which rendering is occurring. */ + private static final double[] PTS = new double[4]; + + /** PaintContext is associated with this graphics context. */ + private final Graphics2D graphics; + + /** Used while computing transparency. */ + protected PStack compositeStack; + + /** Used to optimize clipping region. */ + protected PStack clipStack; + + /** Tracks clipping region in local coordinate system. */ + protected PStack localClipStack; + + /** Stack of cameras through which the node being painted is being viewed. */ + protected PStack cameraStack; + + /** Stack of transforms being applied to the drawing context. */ + protected PStack transformStack; + + /** The current render quality that all rendering should be done in. */ + protected int renderQuality; + + /** + * Creates a PPaintContext associated with the given graphics context. + * + * @param graphics graphics context to associate with this paint context + */ + public PPaintContext(final Graphics2D graphics) { + this.graphics = graphics; + compositeStack = new PStack(); + clipStack = new PStack(); + localClipStack = new PStack(); + cameraStack = new PStack(); + transformStack = new PStack(); + renderQuality = HIGH_QUALITY_RENDERING; + + Shape clip = graphics.getClip(); + if (clip == null) { + clip = new PBounds(-Integer.MAX_VALUE / 2, -Integer.MAX_VALUE / 2, Integer.MAX_VALUE, Integer.MAX_VALUE); + graphics.setClip(clip); + } + + localClipStack.push(clip.getBounds2D()); + + CURRENT_PAINT_CONTEXT = this; + } + + /** + * Returns the graphics context associated with this paint context. + * + * @return graphics context associated with this paint context + */ + public Graphics2D getGraphics() { + return graphics; + } + + /** + * Returns the clipping region in the local coordinate system applied by + * graphics. + * + * @return clipping region in the local coordinate system applied by + * graphics + */ + public Rectangle2D getLocalClip() { + return (Rectangle2D) localClipStack.peek(); + } + + /** + * Returns scale of the current graphics context. By calculating how a unit + * segment gets transformed after transforming it by the graphics context's + * transform. + * + * @return scale of the current graphics context's transformation + */ + public double getScale() { + // x1, y1, x2, y2 + PTS[0] = 0; + PTS[1] = 0; + PTS[2] = 1; + PTS[3] = 0; + graphics.getTransform().transform(PTS, 0, PTS, 0, 2); + return Point2D.distance(PTS[0], PTS[1], PTS[2], PTS[3]); + } + + /** + * Pushes the camera onto the camera stack. + * + * @param aCamera camera to push onto the stack + */ + public void pushCamera(final PCamera aCamera) { + cameraStack.push(aCamera); + } + + /** + * @deprecated in favor of popCamera() + * + * @param aCamera absolute not used in any way + */ + public void popCamera(final PCamera aCamera) { + cameraStack.pop(); + } + + /** + * Removes the camera at the top of the camera stack. + * + * @since 1.3 + */ + public void popCamera() { + cameraStack.pop(); + } + + /** + * Returns the camera at the top of the camera stack, or null if stack is + * empty. + * + * @return topmost camera on camera stack or null if stack is empty + */ + public PCamera getCamera() { + return (PCamera) cameraStack.peek(); + } + + /** + * Pushes the given clip to the pain context. + * + * @param clip clip to be pushed + */ + public void pushClip(final Shape clip) { + final Shape currentClip = graphics.getClip(); + clipStack.push(currentClip); + graphics.clip(clip); + final Rectangle2D newLocalClip = clip.getBounds2D(); + Rectangle2D.intersect(getLocalClip(), newLocalClip, newLocalClip); + localClipStack.push(newLocalClip); + } + + /** + * Removes the topmost clipping region from the clipping stack. + * + * @param clip not used in this method + */ + public void popClip(final Shape clip) { + final Shape newClip = (Shape) clipStack.pop(); + graphics.setClip(newClip); + localClipStack.pop(); + } + + /** + * Pushes the provided transparency onto the transparency stack if + * necessary. If the transparency is fully opaque, then it does nothing. + * + * @param transparency transparency to be pushed onto the transparency stack + */ + public void pushTransparency(final float transparency) { + if (transparency == 1.0f) { + return; + } + final Composite current = graphics.getComposite(); + float currentAlaph = 1.0f; + compositeStack.push(current); + + if (current instanceof AlphaComposite) { + currentAlaph = ((AlphaComposite) current).getAlpha(); + } + final AlphaComposite newComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, currentAlaph + * transparency); + graphics.setComposite(newComposite); + } + + /** + * Removes the topmost transparency if the given transparency is not opaque + * (1f). + * + * @param transparency transparency to be popped + */ + public void popTransparency(final float transparency) { + if (transparency == 1.0f) { + return; + } + final Composite c = (Composite) compositeStack.pop(); + graphics.setComposite(c); + } + + /** + * Pushed the provided transform onto the transform stack if it is not null. + * + * @param transform will be pushed onto the transform stack if not null + */ + public void pushTransform(final PAffineTransform transform) { + if (transform != null) { + final Rectangle2D newLocalClip = (Rectangle2D) getLocalClip().clone(); + transform.inverseTransform(newLocalClip, newLocalClip); + transformStack.push(graphics.getTransform()); + localClipStack.push(newLocalClip); + graphics.transform(transform); + } + } + + /** + * Pops the topmost Transform from the top of the transform if the passed in + * transform is not null. + * + * @param transform transform that should be at the top of the stack + */ + public void popTransform(final PAffineTransform transform) { + if (transform != null) { + graphics.setTransform((AffineTransform) transformStack.pop()); + localClipStack.pop(); + } + } + + /** + * Return the render quality used by this paint context. + * + * @return the current render quality + */ + public int getRenderQuality() { + return renderQuality; + } + + /** + * Set the rendering hints for this paint context. The render quality is + * most often set by the rendering PCanvas. Use PCanvas.setRenderQuality() + * and PCanvas.setInteractingRenderQuality() to set these values. + * + * @param requestedQuality supports PPaintContext.HIGH_QUALITY_RENDERING or + * PPaintContext.LOW_QUALITY_RENDERING + */ + public void setRenderQuality(final int requestedQuality) { + renderQuality = requestedQuality; + + switch (renderQuality) { + case HIGH_QUALITY_RENDERING: + setRenderQualityToHigh(); + break; + + case LOW_QUALITY_RENDERING: + setRenderQualityToLow(); + break; + + default: + throw new RuntimeException("Quality must be either HIGH_QUALITY_RENDERING or LOW_QUALITY_RENDERING"); + } + } + + private void setRenderQualityToLow() { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); + graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + } + + private void setRenderQualityToHigh() { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PPickPath.java b/src/main/java/edu/umd/cs/piccolo/util/PPickPath.java new file mode 100644 index 0000000..a9b7fe8 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PPickPath.java @@ -0,0 +1,422 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +import java.awt.geom.Dimension2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.HashMap; + +import javax.swing.event.EventListenerList; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.event.PInputEventListener; + +/** + * PPickPath represents a ordered list of nodes that have been picked. + * The topmost ancestor node is the first node in the list (and should be a + * camera), the bottommost child node is at the end of the list. It is this + * bottom node that is given first chance to handle events, and that any active + * event handlers usually manipulate. + *
+ * Note that because of layers (which can be picked by multiple camera's) the + * ordered list of nodes in a pick path do not all share a parent child + * relationship with the nodes in the list next to them. This means that the + * normal localToGlobal methods don't work when trying to transform geometry up + * and down the pick path, instead you should use the pick paths canvasToLocal + * methods to get the mouse event points into your local coord system. + *
+ * Note that PInputEvent wraps most of the useful PPickPath methods, so often + * you can use a PInputEvent directly instead of having to access its pick path. + *
+ * + * @see edu.umd.cs.piccolo.event.PInputEvent + * @version 1.0 + * @author Jesse Grosjean + */ +public class PPickPath implements PInputEventListener { + /** Global pick path. */ + public static PPickPath CURRENT_PICK_PATH; + + /** Used when calculating the scale. */ + private static final double[] PTS = new double[4]; + + /** Stack of nodes representing all picked nodes. */ + private PStack nodeStack; + + private final PCamera topCamera; + private PStack transformStack; + private PStack pickBoundsStack; + private PCamera bottomCamera; + private HashMap excludedNodes; + + /** + * Creates a pick pack originating from the provided camera and with the + * given screen pick bounds. + * + * @param camera camera from which the pickpath originates + * @param aScreenPickBounds bounds of pick area + */ + public PPickPath(final PCamera camera, final PBounds aScreenPickBounds) { + super(); + pickBoundsStack = new PStack(); + topCamera = camera; + nodeStack = new PStack(); + transformStack = new PStack(); + pickBoundsStack.push(aScreenPickBounds); + + CURRENT_PICK_PATH = this; + } + + /** + * Returns the bounds of the entire PickPath taken as a whole. + * + * @return bounds of the entire PickPath + */ + public PBounds getPickBounds() { + return (PBounds) pickBoundsStack.peek(); + } + + /** + * Determines if the passed node has been excluded from being a member of + * the pickpath. + * + * @param node node being tested + * @return true if node is acceptable to the path + */ + public boolean acceptsNode(final PNode node) { + return excludedNodes == null || !excludedNodes.containsKey(node); + } + + // **************************************************************** + // Picked Nodes + // **************************************************************** + + /** + * Pushes the provided node to the top of the pick path. + * + * @param node node to be added to the pick path + */ + public void pushNode(final PNode node) { + nodeStack.push(node); + } + + /** + * Removes the topmost node from the node stack. + * + * @param node completely unused in this method, but is passed in so that + * subclasses may be informed of it. + */ + public void popNode(final PNode node) { + nodeStack.pop(); + } + + /** + * Get the bottom node on the pick path node stack. That is the last node to + * be picked. + * + * @return the bottom node on the pick path + */ + public PNode getPickedNode() { + return (PNode) nodeStack.peek(); + } + + // **************************************************************** + // Iterating over picked nodes. + // **************************************************************** + + /** + * Return the next node that will be picked after the current picked node. + * For instance of you have two overlapping children nodes then the topmost + * child will always be picked first, use this method to find the covered + * child. Return the camera when no more visual will be picked. + * + * @return next node to picked after the picked node + */ + public PNode nextPickedNode() { + final PNode picked = getPickedNode(); + + if (picked == topCamera) { + return null; + } + if (excludedNodes == null) { + excludedNodes = new HashMap(); + } + + // exclude current picked node + excludedNodes.put(picked, picked); + + final Object screenPickBounds = pickBoundsStack.get(0); + + // reset path state + pickBoundsStack = new PStack(); + nodeStack = new PStack(); + transformStack = new PStack(); + pickBoundsStack = new PStack(); + + pickBoundsStack.push(screenPickBounds); + + // pick again + topCamera.fullPick(this); + + // make sure top camera is pushed. + if (getNodeStackReference().size() == 0) { + pushNode(topCamera); + pushTransform(topCamera.getTransformReference(false)); + } + + return getPickedNode(); + } + + /** + * Get the top camera on the pick path. This is the camera that originated + * the pick action. + * + * @return the topmost camera of this pick pack + */ + public PCamera getTopCamera() { + return topCamera; + } + + /** + * Get the bottom camera on the pick path. This may be different then the + * top camera if internal cameras are in use. + * + * @return the camera closest to the picked node + */ + public PCamera getBottomCamera() { + if (bottomCamera == null) { + bottomCamera = calculateBottomCamera(); + } + return bottomCamera; + } + + private PCamera calculateBottomCamera() { + for (int i = nodeStack.size() - 1; i >= 0; i--) { + final PNode each = (PNode) nodeStack.get(i); + if (each instanceof PCamera) { + return (PCamera) each; + } + } + return null; + } + + /** + * Returns a reference to the node stack. Be Careful! + * + * @return the node stack + */ + public PStack getNodeStackReference() { + return nodeStack; + } + + // **************************************************************** + // Path Transform + // **************************************************************** + + /** + * Returns the resulting scale of applying the transforms of the entire pick + * path. In essence it gives you the scale at which interaction is + * occurring. + * + * @return scale at which interaction is occurring. + */ + public double getScale() { + // x1, y1, x2, y3 + PTS[0] = 0; + PTS[1] = 0; + PTS[2] = 1; + PTS[3] = 0; + + final int count = transformStack.size(); + for (int i = 0; i < count; i++) { + final PAffineTransform each = ((PTuple) transformStack.get(i)).transform; + if (each != null) { + each.transform(PTS, 0, PTS, 0, 2); + } + } + + return Point2D.distance(PTS[0], PTS[1], PTS[2], PTS[3]); + } + + /** + * Adds the transform to the pick path's transform. This is used when + * determining the context of the current interaction. + * + * @param transform transform to be added to applied to the pickpath. + */ + public void pushTransform(final PAffineTransform transform) { + transformStack.push(new PTuple(getPickedNode(), transform)); + if (transform != null) { + final Rectangle2D newPickBounds = (Rectangle2D) getPickBounds().clone(); + transform.inverseTransform(newPickBounds, newPickBounds); + pickBoundsStack.push(newPickBounds); + } + } + + /** + * Pops the top most transform from the pick path. + * + * @param transform unused in this method + */ + public void popTransform(final PAffineTransform transform) { + transformStack.pop(); + if (transform != null) { + pickBoundsStack.pop(); + } + } + + /** + * Calculates the context at which the given node is being interacted with. + * + * @param nodeOnPath a node currently on the pick path. An exception will be + * thrown if the node cannot be found. + * + * @return Transform at which the given node is being interacted with. + */ + public PAffineTransform getPathTransformTo(final PNode nodeOnPath) { + final PAffineTransform aTransform = new PAffineTransform(); + + final int count = transformStack.size(); + for (int i = 0; i < count; i++) { + final PTuple each = (PTuple) transformStack.get(i); + if (each.transform != null) { + aTransform.concatenate(each.transform); + } + if (nodeOnPath == each.node) { + return aTransform; + } + } + + throw new RuntimeException("Node could not be found on pick path"); + } + + /** + * Process Events - Give each node in the pick path, starting at the bottom + * most one, a chance to handle the event. + * + * @param event event to be processed + * @param eventType the type of event being processed + */ + public void processEvent(final PInputEvent event, final int eventType) { + event.setPath(this); + + for (int i = nodeStack.size() - 1; i >= 0; i--) { + final PNode each = (PNode) nodeStack.get(i); + + final EventListenerList list = each.getListenerList(); + + if (list != null) { + final Object[] listeners = list.getListeners(PInputEventListener.class); + + for (int j = 0; j < listeners.length; j++) { + final PInputEventListener listener = (PInputEventListener) listeners[j]; + listener.processEvent(event, eventType); + if (event.isHandled()) { + return; + } + } + } + } + } + + // **************************************************************** + // Transforming Geometry - Methods to transform geometry through + // this path. + //
+ // Note that this is different that just using the + // PNode.localToGlobal (an other coord system transform methods). + // The PNode coord system transform methods always go directly up + // through their parents. The PPickPath coord system transform + // methods go up through the list of picked nodes instead. And since + // cameras can pick their layers in addition to their children these + // two paths may be different. + // **************************************************************** + + /** + * Convert the given point from the canvas coordinates, down through the + * pick path (and through any camera view transforms applied to the path) to + * the local coordinates of the given node. + * + * @param canvasPoint point to be transformed + * @param nodeOnPath node into which the point is to be transformed + * iteratively through the pick path + * + * @return transformed canvasPoint in local coordinates of the picked node + */ + public Point2D canvasToLocal(final Point2D canvasPoint, final PNode nodeOnPath) { + return getPathTransformTo(nodeOnPath).inverseTransform(canvasPoint, canvasPoint); + } + + /** + * Convert the given dimension from the canvas coordinates, down through the + * pick path (and through any camera view transforms applied to the path) to + * the local coordinates of the given node. + * + * @param canvasDimension dimension to be transformed + * @param nodeOnPath node into which the dimension is to be transformed + * iteratively through the stack + * + * @return transformed canvasDimension in local coordinates of the picked + * node + */ + public Dimension2D canvasToLocal(final Dimension2D canvasDimension, final PNode nodeOnPath) { + return getPathTransformTo(nodeOnPath).inverseTransform(canvasDimension, canvasDimension); + } + + /** + * Convert the given rectangle from the canvas coordinates, down through the + * pick path (and through any camera view transforms applied to the path) to + * the local coordinates of the given node. + * + * @param canvasRectangle rectangle to be transformed + * @param nodeOnPath node into which the rectangle is to be transformed + * iteratively through the stack + * @return transformed canvasRectangle in local coordinates of the picked + * node + */ + public Rectangle2D canvasToLocal(final Rectangle2D canvasRectangle, final PNode nodeOnPath) { + return getPathTransformTo(nodeOnPath).inverseTransform(canvasRectangle, canvasRectangle); + } + + /** + * Used to associated nodes with their transforms on the transform stack. + */ + private static class PTuple { + public PNode node; + public PAffineTransform transform; + + public PTuple(final PNode n, final PAffineTransform t) { + node = n; + transform = t; + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PStack.java b/src/main/java/edu/umd/cs/piccolo/util/PStack.java new file mode 100644 index 0000000..772df8e --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PStack.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +import java.util.ArrayList; + +/** + * PStack this class should be removed when a non thread safe stack is + * added to the java class libraries. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PStack extends ArrayList { + /** + * Allows for future serialization code to understand versioned binary + * formats. + */ + private static final long serialVersionUID = 1L; + + /** + * Creates an empty stack. + */ + public PStack() { + } + + /** + * Pushes the provided object onto the top of the stack. + * + * @param o object to add to the stack + */ + public void push(final Object o) { + add(o); + } + + /** + * Returns topmost element on the stack, or null if stack is empty. + * + * @return topmost element on the stack, or null if empty + */ + public Object peek() { + final int s = size(); + if (s == 0) { + return null; + } + else { + return get(s - 1); + } + } + + /** + * Removes top element on the stack and returns it. + * + * @return topmost element on stack. + */ + public Object pop() { + return remove(size() - 1); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/PUtil.java b/src/main/java/edu/umd/cs/piccolo/util/PUtil.java new file mode 100644 index 0000000..79da6d7 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/PUtil.java @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolo.util; + +import java.awt.BasicStroke; +import java.awt.Stroke; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PLayer; +import edu.umd.cs.piccolo.PRoot; + +/** + * PUtil util methods for the Piccolo framework. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PUtil { + /** + * PActivities are broken into steps, this is how many milliseconds should + * pass between steps. + */ + public static long DEFAULT_ACTIVITY_STEP_RATE = 20; + + /** Rate in milliseconds at which the activity timer will get invoked. */ + public static int ACTIVITY_SCHEDULER_FRAME_DELAY = 10; + + /** An iterator that iterates over an empty collection. */ + public static Iterator NULL_ITERATOR = Collections.EMPTY_LIST.iterator(); + + /** + * Used when persisting paths to an object stream. Used to mark the end of + * the path. + */ + private static final int PATH_TERMINATOR = -1; + + /** A utility enumeration with no elements. */ + public static Enumeration NULL_ENUMERATION = new Enumeration() { + public boolean hasMoreElements() { + return false; + } + + public Object nextElement() { + return null; + } + }; + + /** + * @deprecated This has been moved into a private static class of + * PObjectOutputStream + */ + public static OutputStream NULL_OUTPUT_STREAM = new OutputStream() { + public void close() { + } + + public void flush() { + } + + public void write(final byte[] b) { + } + + public void write(final byte[] b, final int off, final int len) { + } + + public void write(final int b) { + } + }; + + /** + * Creates the simplest possible scene graph. 1 Camera, 1 Layer, 1 Root + * + * @return a basic scene with 1 camera, layer and root + */ + public static PCamera createBasicScenegraph() { + final PRoot root = new PRoot(); + final PLayer layer = new PLayer(); + final PCamera camera = new PCamera(); + + root.addChild(camera); + root.addChild(layer); + camera.addLayer(layer); + + return camera; + } + + /** + * Serializes the given stroke object to the object output stream provided. + * By default strokes are not serializable. This method solves that problem. + * + * @param stroke stroke to be serialize + * @param out stream to which the stroke is to be serialized + * @throws IOException can occur if exception occurs with underlying output + * stream + */ + public static void writeStroke(final Stroke stroke, final ObjectOutputStream out) throws IOException { + if (stroke instanceof Serializable) { + out.writeBoolean(true); + out.writeBoolean(true); + out.writeObject(stroke); + } + else if (stroke instanceof BasicStroke) { + out.writeBoolean(true); + out.writeBoolean(false); + writeBasicStroke((BasicStroke) stroke, out); + } + else { + out.writeBoolean(false); + } + } + + private static void writeBasicStroke(final BasicStroke basicStroke, final ObjectOutputStream out) + throws IOException { + final float[] dash = basicStroke.getDashArray(); + + if (dash == null) { + out.write(0); + } + else { + out.write(dash.length); + for (int i = 0; i < dash.length; i++) { + out.writeFloat(dash[i]); + } + } + + out.writeFloat(basicStroke.getLineWidth()); + out.writeInt(basicStroke.getEndCap()); + out.writeInt(basicStroke.getLineJoin()); + out.writeFloat(basicStroke.getMiterLimit()); + out.writeFloat(basicStroke.getDashPhase()); + } + + /** + * Reconstitutes a stroke from the provided Object Input Stream. According + * to the scheme found in writeStroke. By default strokes are not + * serializable. + * + * @param in stream from which Stroke is to be read + * @return a stroke object + * @throws IOException occurs if an exception occurs reading from in stream + * @throws ClassNotFoundException should never happen, but can if somehow + * the stroke class is not on the classpath + */ + public static Stroke readStroke(final ObjectInputStream in) throws IOException, ClassNotFoundException { + final boolean wroteStroke = in.readBoolean(); + if (!wroteStroke) { + return null; + } + + final boolean serializedStroke = in.readBoolean(); + if (serializedStroke) { + return (Stroke) in.readObject(); + } + + return readBasicStroke(in); + } + + private static Stroke readBasicStroke(final ObjectInputStream in) throws IOException { + float[] dash = null; + final int dashLength = in.read(); + + if (dashLength != 0) { + dash = new float[dashLength]; + for (int i = 0; i < dashLength; i++) { + dash[i] = in.readFloat(); + } + } + + final float lineWidth = in.readFloat(); + final int endCap = in.readInt(); + final int lineJoin = in.readInt(); + final float miterLimit = in.readFloat(); + final float dashPhase = in.readFloat(); + + return new BasicStroke(lineWidth, endCap, lineJoin, miterLimit, dash, dashPhase); + } + + /** + * Reads a path from the provided inputStream in accordance with the + * serialization policy defined in writePath. + * + * @param in stream from which to read the path. + * @return reconstituted path + * @throws IOException if an unknown path type is read from the stream + * @throws ClassNotFoundException should never happen, but can if somehow + * the classpath is seriously messed up + */ + public static GeneralPath readPath(final ObjectInputStream in) throws IOException, ClassNotFoundException { + final GeneralPath path = new GeneralPath(); + + while (true) { + final int segType = in.readInt(); + + switch (segType) { + case PathIterator.SEG_MOVETO: + path.moveTo(in.readFloat(), in.readFloat()); + break; + + case PathIterator.SEG_LINETO: + path.lineTo(in.readFloat(), in.readFloat()); + break; + + case PathIterator.SEG_QUADTO: + path.quadTo(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat()); + break; + + case PathIterator.SEG_CUBICTO: + path.curveTo(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in + .readFloat()); + break; + + case PathIterator.SEG_CLOSE: + path.closePath(); + break; + + case PATH_TERMINATOR: + return path; + + default: + throw new IOException("Unknown path type encountered while deserializing path."); + } + } + } + + /** + * Serializes the given path to the provided Object Output Stream. + * + * @param path path to be serialized + * @param out stream to which the path should be serialized + * @throws IOException if unknown path segment type is encountered, or an + * exception occurs writing to the output stream + */ + public static void writePath(final GeneralPath path, final ObjectOutputStream out) throws IOException { + final PathIterator i = path.getPathIterator(null); + final float[] data = new float[6]; + + while (!i.isDone()) { + switch (i.currentSegment(data)) { + case PathIterator.SEG_MOVETO: + out.writeInt(PathIterator.SEG_MOVETO); + out.writeFloat(data[0]); + out.writeFloat(data[1]); + break; + + case PathIterator.SEG_LINETO: + out.writeInt(PathIterator.SEG_LINETO); + out.writeFloat(data[0]); + out.writeFloat(data[1]); + break; + + case PathIterator.SEG_QUADTO: + out.writeInt(PathIterator.SEG_QUADTO); + out.writeFloat(data[0]); + out.writeFloat(data[1]); + out.writeFloat(data[2]); + out.writeFloat(data[3]); + break; + + case PathIterator.SEG_CUBICTO: + out.writeInt(PathIterator.SEG_CUBICTO); + out.writeFloat(data[0]); + out.writeFloat(data[1]); + out.writeFloat(data[2]); + out.writeFloat(data[3]); + out.writeFloat(data[4]); + out.writeFloat(data[5]); + break; + + case PathIterator.SEG_CLOSE: + out.writeInt(PathIterator.SEG_CLOSE); + break; + + default: + throw new IOException("Unknown path type encountered while serializing path."); + } + + i.next(); + } + + out.writeInt(PATH_TERMINATOR); + } +} diff --git a/src/main/java/edu/umd/cs/piccolo/util/package.html b/src/main/java/edu/umd/cs/piccolo/util/package.html new file mode 100644 index 0000000..c6af443 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolo/util/package.html @@ -0,0 +1,5 @@ +
+This package defines several utility classes that are likely +to be useful for Piccolo applications. These utility classes are +also used within the implementation of Piccolo. + diff --git a/src/main/java/edu/umd/cs/piccolox/PApplet.java b/src/main/java/edu/umd/cs/piccolox/PApplet.java new file mode 100644 index 0000000..92a3860 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/PApplet.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox; + +import javax.swing.JApplet; +import javax.swing.SwingUtilities; + +import edu.umd.cs.piccolo.PCanvas; + +/** + * PApplet is meant to be subclassed by applications that just need a + * PCanvas embedded in a web page. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PApplet extends JApplet { + /** Used to allow versioned binary streams for serializations. */ + private static final long serialVersionUID = 1L; + + /** Canvas being displayed by this applet. */ + private PCanvas canvas; + + /** + * Initializes the applet with a canvas and no background. + */ + public void init() { + setBackground(null); + + canvas = createCanvas(); + getContentPane().add(canvas); + validate(); + canvas.requestFocus(); + beforeInitialize(); + + // Manipulation of Piccolo's scene graph should be done from Swings + // event dispatch thread since Piccolo is not thread safe. This code + // calls initialize() from that thread once the PFrame is initialized, + // so you are safe to start working with Piccolo in the initialize() + // method. + SwingUtilities.invokeLater(new Runnable() { + public void run() { + PApplet.this.initialize(); + repaint(); + } + }); + } + + /** + * Returns the canvas this PApplet is displaying. + * + * @return canvas this applet is displaying + */ + public PCanvas getCanvas() { + return canvas; + } + + /** + * Provides an extension point for subclasses so that they can control + * what's on the canvas by default. + * + * @return a built canvas + */ + public PCanvas createCanvas() { + return new PCanvas(); + } + + /** + * This method will be called before the initialize() method and will be + * called on the thread that is constructing this object. + */ + public void beforeInitialize() { + } + + /** + * Subclasses should override this method and add their Piccolo2d + * initialization code there. This method will be called on the swing event + * dispatch thread. Note that the constructors of PFrame subclasses may not + * be complete when this method is called. If you need to initailize some + * things in your class before this method is called place that code in + * beforeInitialize(); + */ + public void initialize() { + } +} \ No newline at end of file diff --git a/src/main/java/edu/umd/cs/piccolox/PFrame.java b/src/main/java/edu/umd/cs/piccolox/PFrame.java new file mode 100644 index 0000000..b6adf1f --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/PFrame.java @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox; + +import java.awt.Dimension; +import java.awt.DisplayMode; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventListener; +import java.util.Iterator; + +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +import edu.umd.cs.piccolo.PCanvas; + +/** + * PFrame is meant to be subclassed by applications that just need a + * PCanvas in a JFrame. It also includes full screen mode functionality when run + * in JDK 1.4. These subclasses should override the initialize method and start + * adding their own code there. Look in the examples package to see lots of uses + * of PFrame. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PFrame extends JFrame { + private static final Dimension DEFAULT_FRAME_DIMENSION = new Dimension(400, 400); + + private static final Point DEFAULT_FRAME_POSITION = new Point(100, 100); + + /** Used to allow versioned binary streams for serializations. */ + private static final long serialVersionUID = 1L; + + /** Canvas being displayed on this PFrame. */ + private PCanvas canvas; + + /** The graphics device onto which the PFrame is being displayed. */ + private final GraphicsDevice graphicsDevice; + + /** Listener that listens for escape key. */ + private transient EventListener escapeFullScreenModeListener; + + /** + * Creates a PFrame with no title, not full screen, and with the default + * canvas. + */ + public PFrame() { + this("", false, null); + } + + /** + * Creates a PFrame with the given title and with the default canvas. + * + * @param title title to display at the top of the frame + * @param fullScreenMode whether to display a full screen frame or not + * @param canvas to embed in the frame + */ + public PFrame(final String title, final boolean fullScreenMode, final PCanvas canvas) { + this(title, GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(), fullScreenMode, canvas); + } + + /** + * Creates a PFrame with the given title and with the default canvas being + * displayed on the provided device. + * + * @param title title to display at the top of the frame + * @param device device onto which PFrame is to be displayed + * @param fullScreen whether to display a full screen frame or not + * @param canvas to embed in the frame, may be null. If so, it'll create a + * default PCanvas + */ + public PFrame(final String title, final GraphicsDevice device, final boolean fullScreen, final PCanvas canvas) { + super(title, device.getDefaultConfiguration()); + + graphicsDevice = device; + + setBackground(null); + setBounds(getDefaultFrameBounds()); + + try { + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + catch (final SecurityException e) { + // expected from Applets + System.out.println("Ignoring security exception. Assuming Applet Context."); + } + + if (canvas == null) { + this.canvas = new PCanvas(); + } + else { + this.canvas = canvas; + } + + setContentPane(this.canvas); + validate(); + setFullScreenMode(fullScreen); + this.canvas.requestFocus(); + beforeInitialize(); + + // Manipulation of Piccolo's scene graph should be done from Swings + // event dispatch thread since Piccolo2D is not thread safe. This code + // calls initialize() from that thread once the PFrame is initialized, + // so you are safe to start working with Piccolo2D in the initialize() + // method. + SwingUtilities.invokeLater(new Runnable() { + public void run() { + PFrame.this.initialize(); + repaint(); + } + }); + } + + /** + * Returns the canvas being displayed on this frame. + * + * @return canvas being displayed on this frame + */ + public PCanvas getCanvas() { + return canvas; + } + + /** + * Returns the default frame bounds. + * + * @return default frame bounds + */ + public Rectangle getDefaultFrameBounds() { + return new Rectangle(DEFAULT_FRAME_POSITION, DEFAULT_FRAME_DIMENSION); + } + + /** + * Returns whether the frame is currently in full screen mode. + * + * @return whether the frame is currently in full screen mode + */ + public boolean isFullScreenMode() { + return graphicsDevice.getFullScreenWindow() != null; + } + + /** + * Switches full screen state. + * + * @param fullScreenMode whether to place the frame in full screen mode or + * not. + */ + public void setFullScreenMode(final boolean fullScreenMode) { + if (fullScreenMode != isFullScreenMode() || !isVisible()) { + if (fullScreenMode) { + switchToFullScreenMode(); + } + else { + switchToWindowedMode(); + } + } + } + + private void switchToFullScreenMode() { + addEscapeFullScreenModeListener(); + + if (isDisplayable()) { + dispose(); + } + + setUndecorated(true); + setResizable(false); + graphicsDevice.setFullScreenWindow(this); + + if (graphicsDevice.isDisplayChangeSupported()) { + chooseBestDisplayMode(graphicsDevice); + } + validate(); + } + + private void switchToWindowedMode() { + removeEscapeFullScreenModeListener(); + + if (isDisplayable()) { + dispose(); + } + + setUndecorated(false); + setResizable(true); + graphicsDevice.setFullScreenWindow(null); + validate(); +// setVisible(true); //TODO: miuramo modified disabled 2012.9.21 + } + + /** + * Sets the display mode to the best device mode that can be determined. + * + * Used in full screen mode. + * + * @param device The graphics device being controlled. + */ + protected void chooseBestDisplayMode(final GraphicsDevice device) { + final DisplayMode best = getBestDisplayMode(device); + if (best != null) { + device.setDisplayMode(best); + } + } + + /** + * Finds the best display mode the graphics device supports. Based on the + * preferred modes. + * + * @param device the device being inspected + * + * @return best display mode the given device supports + */ + protected DisplayMode getBestDisplayMode(final GraphicsDevice device) { + final Iterator itr = getPreferredDisplayModes(device).iterator(); + while (itr.hasNext()) { + final DisplayMode each = (DisplayMode) itr.next(); + final DisplayMode[] modes = device.getDisplayModes(); + for (int i = 0; i < modes.length; i++) { + if (modes[i].getWidth() == each.getWidth() && modes[i].getHeight() == each.getHeight() + && modes[i].getBitDepth() == each.getBitDepth()) { + return each; + } + } + } + + return null; + } + + /** + * By default return the current display mode. Subclasses may override this + * method to return other modes in the collection. + * + * @param device the device being inspected + * @return preferred display mode + */ + protected Collection getPreferredDisplayModes(final GraphicsDevice device) { + final ArrayList result = new ArrayList(); + + result.add(device.getDisplayMode()); + /* + * result.add(new DisplayMode(640, 480, 32, 0)); result.add(new + * DisplayMode(640, 480, 16, 0)); result.add(new DisplayMode(640, 480, + * 8, 0)); + */ + + return result; + } + + /** + * This method adds a key listener that will take this PFrame out of full + * screen mode when the escape key is pressed. This is called for you + * automatically when the frame enters full screen mode. + */ + public void addEscapeFullScreenModeListener() { + removeEscapeFullScreenModeListener(); + escapeFullScreenModeListener = new KeyAdapter() { + public void keyPressed(final KeyEvent aEvent) { + if (aEvent.getKeyCode() == KeyEvent.VK_ESCAPE) { + setFullScreenMode(false); + } + } + }; + canvas.addKeyListener((KeyListener) escapeFullScreenModeListener); + } + + /** + * This method removes the escape full screen mode key listener. It will be + * called for you automatically when full screen mode exits, but the method + * has been made public for applications that wish to use other methods for + * exiting full screen mode. + */ + public void removeEscapeFullScreenModeListener() { + if (escapeFullScreenModeListener != null) { + canvas.removeKeyListener((KeyListener) escapeFullScreenModeListener); + escapeFullScreenModeListener = null; + } + } + + // **************************************************************** + // Initialize + // **************************************************************** + + /** + * This method will be called before the initialize() method and will be + * called on the thread that is constructing this object. + */ + public void beforeInitialize() { + } + + /** + * Subclasses should override this method and add their Piccolo2D + * initialization code there. This method will be called on the swing event + * dispatch thread. Note that the constructors of PFrame subclasses may not + * be complete when this method is called. If you need to initialize some + * things in your class before this method is called place that code in + * beforeInitialize(); + */ + public void initialize() { + } + + /** + * Method for testing the creating of PFrame. + * + * @deprecated since it's not terribly useful + * + * @param argv command line arguments + */ + public static void main(final String[] argv) { + new PFrame(); + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/activities/PPathActivity.java b/src/main/java/edu/umd/cs/piccolox/activities/PPathActivity.java new file mode 100644 index 0000000..e25b178 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/activities/PPathActivity.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.activities; + +import edu.umd.cs.piccolo.activities.PInterpolatingActivity; + +/** + * PPathActivity is the abstract base class for all path activity + * interpolators. Path activities interpolate between multiple states over the + * duration of the activity. + *+ * Knots are used to determine when in time the activity should move from state + * to state. Knot values should be increasing in value from 0 to 1 inclusive. + * This class is based on the Java 3D PathInterpolator object, see that class + * documentation for more information on the basic concepts used in this classes + * design. + *
+ *+ * See PPositionPathActivity for a concrete path activity that will animate + * through a list of points. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public abstract class PPathActivity extends PInterpolatingActivity { + + /** + * The "knots" that define this path's activity timing through its activity + * and should be an monotonously increasing array starting where each value + * is >=0 and ending at 1f. + */ + protected float[] knots; + + /** + * Constructs a PPathActivity that will last the specified duration, will + * animate every stepRate and will progress according to the knots provided. + * + * @param duration duration in milliseconds that this activity should last + * @param stepRate interval in milliseconds between animation steps + * @param knots array defining the speed of the animation alongs it's + * animation + */ + public PPathActivity(final long duration, final long stepRate, final float[] knots) { + this(duration, stepRate, 0, PInterpolatingActivity.SOURCE_TO_DESTINATION, knots); + } + + /** + * Constructs a PPathActivity that will repeat the specified number of + * times, last the specified duration, will animate every stepRate and will + * progress according to the knots provided. + * + * @param duration duration in milliseconds that this activity should last + * @param stepRate interval in milliseconds between animation steps + * @param knots array defining the speed of the animation alongs it's + * animation + * @param loopCount # of times activity should repeat + * @param mode controls easing of the activity + */ + public PPathActivity(final long duration, final long stepRate, final int loopCount, final int mode, + final float[] knots) { + super(duration, stepRate, loopCount, mode); + setKnots(knots); + } + + /** + * Returns the number of knots that define the timing of this activity. + * + * @return # of knots + */ + public int getKnotsLength() { + return knots.length; + } + + /** + * Changes the knots that define the timing of this activity. + * + * @param newKnots the new knots to assign to this activity + */ + public void setKnots(final float[] newKnots) { + if (newKnots == null) { + this.knots = null; + } + else { + this.knots = (float[]) newKnots.clone(); + } + } + + /** + * Return the knots that define the timing of this activity. + * + * @return new knots + */ + public float[] getKnots() { + if (knots == null) { + return null; + } + return (float[]) knots.clone(); + } + + /** + * Changes the knot at the given index. + * + * @param index index of knot to change + * @param knot new value to assign to the knot + */ + public void setKnot(final int index, final float knot) { + knots[index] = knot; + } + + /** + * Returns the value of the knot at the given index. + * + * @param index index of desired knot + * @return value of knot at given index + */ + public float getKnot(final int index) { + return knots[index]; + } + + /** + * Sets the target's value taking knot timing into account. + * + * @param zeroToOne how much of this activity has elapsed 0=none, + * 1=completed + */ + public void setRelativeTargetValue(final float zeroToOne) { + int currentKnotIndex = 0; + + while (zeroToOne > knots[currentKnotIndex]) { + currentKnotIndex++; + } + + int startKnot = currentKnotIndex - 1; + int endKnot = currentKnotIndex; + + if (startKnot < 0) { + startKnot = 0; + } + if (endKnot > getKnotsLength() - 1) { + endKnot = getKnotsLength() - 1; + } + + final float currentRange = knots[endKnot] - knots[startKnot]; + final float currentPointOnRange = zeroToOne - knots[startKnot]; + float normalizedPointOnRange = currentPointOnRange; + + if (currentRange != 0) { + normalizedPointOnRange = currentPointOnRange / currentRange; + } + + setRelativeTargetValue(normalizedPointOnRange, startKnot, endKnot); + } + + /** + * An abstract method that allows subclasses to define what target value + * matches the given progress and knots. + * + * @param zeroToOne how far between the knots the activity is + * @param startKnot knot that defines the start of this particular interpolation + * @param endKnot knot that defines the end of this particular interpolation + */ + public abstract void setRelativeTargetValue(float zeroToOne, int startKnot, int endKnot); +} diff --git a/src/main/java/edu/umd/cs/piccolox/activities/PPositionPathActivity.java b/src/main/java/edu/umd/cs/piccolox/activities/PPositionPathActivity.java new file mode 100644 index 0000000..f79dc49 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/activities/PPositionPathActivity.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.activities; + +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.util.ArrayList; + +import edu.umd.cs.piccolo.activities.PInterpolatingActivity; + +/** + * PPositionPathActivity animates through a sequence of points. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PPositionPathActivity extends PPathActivity { + /** Points that define the animation's path. */ + protected Point2D[] positions; + + /** An abstract representation of the thing being positioned. */ + protected Target target; + + /** + * Interface that objects must conform to in order to have their position + * animated. + */ + public interface Target { + /** + * Set's the target's position to the coordinate provided. + * + * @param x the x component of the new position + * @param y the y component of the new position + */ + void setPosition(double x, double y); + } + + /** + * Constructs a position activity that acts on the given target for the + * duration provided and will update it's position at the given stepRate. + * + * @param duration milliseconds of animation + * @param stepRate milliseconds between successive position updates + * @param target abstract representation of thing being animated + */ + public PPositionPathActivity(final long duration, final long stepRate, final Target target) { + this(duration, stepRate, target, null, new Point2D[0]); + } + + /** + * Constructs a position activity that acts on the given target for the + * duration provided and will update it's position at the given stepRate. It + * will follow the path defined by the knots and positions arguments. + * + * @param duration milliseconds of animation + * @param stepRate milliseconds between successive position updates + * @param target abstract representation of thing being animated + * @param knots timing to use when animating + * @param positions points along the path + */ + public PPositionPathActivity(final long duration, final long stepRate, final Target target, final float[] knots, + final Point2D[] positions) { + this(duration, stepRate, 1, PInterpolatingActivity.SOURCE_TO_DESTINATION, target, knots, positions); + } + + /** + * Constructs a position activity that will repeat the number of times + * specified. It will act on the given target for the duration provided and + * will update it's position at the given stepRate. It will follow the path + * defined by the knots and positions arguments. + * + * @param duration milliseconds of animation + * @param stepRate milliseconds between successive position updates + * @param loopCount number of times this activity should repeat + * @param mode how easing is handled on this activity + * @param target abstract representation of thing being animated + * @param knots timing to use when animating + * @param positions points along the path + */ + public PPositionPathActivity(final long duration, final long stepRate, final int loopCount, final int mode, + final Target target, final float[] knots, final Point2D[] positions) { + super(duration, stepRate, loopCount, mode, knots); + this.target = target; + this.positions = (Point2D[]) positions.clone(); + } + + /** + * Returns true since this activity modifies the view and so cause a + * repaint. + * + * @return always true + */ + protected boolean isAnimation() { + return true; + } + + /** + * Returns a copy of the path's points. + * + * @return array of points on the path + */ + public Point2D[] getPositions() { + return (Point2D[]) positions.clone(); + } + + /** + * Returns the point at the given index. + * + * @param index desired position index + * @return point at the given index + */ + public Point2D getPosition(final int index) { + return positions[index]; + } + + /** + * Changes all positions that define where along the target is being + * positioned during the animation. + * + * @param positions new animation positions + */ + public void setPositions(final Point2D[] positions) { + this.positions = (Point2D[]) positions.clone(); + } + + /** + * Sets the position of the point at the given index. + * + * @param index index of the point to change + * @param position point defining the new position + */ + public void setPosition(final int index, final Point2D position) { + positions[index] = position; + } + + /** + * Extracts positions from a GeneralPath and uses them to define this + * activity's animation points. + * + * @param path source of points + */ + public void setPositions(final GeneralPath path) { + final PathIterator pi = path.getPathIterator(null, 1); + final ArrayList points = new ArrayList(); + final float[] point = new float[6]; + float distanceSum = 0; + float lastMoveToX = 0; + float lastMoveToY = 0; + + while (!pi.isDone()) { + final int type = pi.currentSegment(point); + + switch (type) { + case PathIterator.SEG_MOVETO: + points.add(new Point2D.Float(point[0], point[1])); + lastMoveToX = point[0]; + lastMoveToY = point[1]; + break; + + case PathIterator.SEG_LINETO: + points.add(new Point2D.Float(point[0], point[1])); + break; + + case PathIterator.SEG_CLOSE: + points.add(new Point2D.Float(lastMoveToX, lastMoveToY)); + break; + + case PathIterator.SEG_QUADTO: + case PathIterator.SEG_CUBICTO: + throw new RuntimeException(); + default: + // ok to do nothing it'll just be skipped + } + + if (points.size() > 1) { + final Point2D last = (Point2D) points.get(points.size() - 2); + final Point2D current = (Point2D) points.get(points.size() - 1); + distanceSum += last.distance(current); + } + + pi.next(); + } + + final int size = points.size(); + final Point2D[] newPositions = new Point2D[size]; + final float[] newKnots = new float[size]; + + for (int i = 0; i < size; i++) { + newPositions[i] = (Point2D) points.get(i); + if (i > 0) { + final float dist = (float) newPositions[i - 1].distance(newPositions[i]); + newKnots[i] = newKnots[i - 1] + dist / distanceSum; + } + } + + setPositions(newPositions); + setKnots(newKnots); + } + + /** + * Overridden to interpret position at correct point along animation. + * + * TODO: improve these comments + * + * @param zeroToOne how far along the activity we are + * @param startKnot the index of the startKnot + * @param endKnot the index of the endKnot + */ + public void setRelativeTargetValue(final float zeroToOne, final int startKnot, final int endKnot) { + final Point2D start = getPosition(startKnot); + final Point2D end = getPosition(endKnot); + target.setPosition(start.getX() + zeroToOne * (end.getX() - start.getX()), start.getY() + zeroToOne + * (end.getY() - start.getY())); + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/event/PNavigationEventHandler.java b/src/main/java/edu/umd/cs/piccolox/event/PNavigationEventHandler.java new file mode 100644 index 0000000..4fe8503 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/event/PNavigationEventHandler.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.event; + +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.activities.PActivity; +import edu.umd.cs.piccolo.activities.PTransformActivity; +import edu.umd.cs.piccolo.event.PBasicInputEventHandler; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.event.PInputEventFilter; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PDimension; + +/** + * PNavigationEventHandler implements simple focus based navigation. Uses + * mouse button one or the arrow keys to set a new focus. Animates the canvas + * view to keep the focus node on the screen and at 100 percent scale with + * minimal view movement. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PNavigationEventHandler extends PBasicInputEventHandler { + /** Minum size under which two scales are considered the same. */ + private static final double SCALING_THRESHOLD = 0.0001; + /** Amount of time it takes to animation view from one location to another. */ + private static final int NAVIGATION_DURATION = 500; + /** The UP direction on the screen. */ + public static final int NORTH = 0; + /** The DOWN direction on the screen. */ + public static final int SOUTH = 1; + /** The RIGHT direction on the screen. */ + public static final int EAST = 2; + /** The LEFT direction on the screen. */ + public static final int WEST = 3; + /** The IN direction on the scene. */ + public static final int IN = 4; + /** The OUT direction on the scene. */ + public static final int OUT = 5; + + private static Hashtable NODE_TO_GLOBAL_NODE_CENTER_MAPPING = new Hashtable(); + + private PNode focusNode; + private PTransformActivity navigationActivity; + + /** + * Constructs a Navigation Event Handler that will only accepts left mouse + * clicks. + */ + public PNavigationEventHandler() { + super(); + setEventFilter(new PInputEventFilter(InputEvent.BUTTON1_MASK)); + } + + // **************************************************************** + // Focus Change Events. + // **************************************************************** + + /** + * Processes key pressed events. + * + * @param event event representing the key press + */ + public void keyPressed(final PInputEvent event) { + final PNode oldLocation = focusNode; + + switch (event.getKeyCode()) { + case KeyEvent.VK_LEFT: + moveFocusLeft(event); + break; + + case KeyEvent.VK_RIGHT: + moveFocusRight(event); + break; + + case KeyEvent.VK_UP: + case KeyEvent.VK_PAGE_UP: + if (event.isAltDown()) { + moveFocusOut(event); + } + else { + moveFocusUp(event); + } + break; + + case KeyEvent.VK_DOWN: + case KeyEvent.VK_PAGE_DOWN: + if (event.isAltDown()) { + moveFocusIn(event); + } + else { + moveFocusDown(event); + } + break; + default: + // Pressed key is not a navigation key. + } + + if (focusNode != null && oldLocation != focusNode) { + directCameraViewToFocus(event.getCamera(), focusNode, NAVIGATION_DURATION); + } + } + + /** + * Animates the camera to the node that has been pressed. + * + * @param event event representing the mouse press + */ + public void mousePressed(final PInputEvent event) { + moveFocusToMouseOver(event); + + if (focusNode != null) { + directCameraViewToFocus(event.getCamera(), focusNode, NAVIGATION_DURATION); + event.getInputManager().setKeyboardFocus(event.getPath()); + } + } + + // **************************************************************** + // Focus Movement - Moves the focus the specified direction. Left, + // right, up, down mean move the focus to the closest sibling of the + // current focus node that exists in that direction. Move in means + // move the focus to a child of the current focus, move out means + // move the focus to the parent of the current focus. + // **************************************************************** + + /** + * Moves the focus in the downward direction. Animating the camera + * accordingly. + * + * @param event ignored + */ + public void moveFocusDown(final PInputEvent event) { + moveFocusInDirection(SOUTH); + } + + /** + * Moves the focus "into" the scene. So smaller nodes appear larger on + * screen. Animates the camera accordingly. + * + * @param event ignored + */ + public void moveFocusIn(final PInputEvent event) { + moveFocusInDirection(IN); + } + + /** + * Moves the focus in the left direction. Animating the camera accordingly. + * + * @param event ignored + */ + public void moveFocusLeft(final PInputEvent event) { + moveFocusInDirection(WEST); + } + + /** + * Moves the focus "out" of scene. So larger nodes appear smaller on screen. + * Animates the camera accordingly. + * + * @param event ignored + */ + public void moveFocusOut(final PInputEvent event) { + moveFocusInDirection(OUT); + } + + /** + * Moves the focus in the right direction. Animating the camera accordingly. + * + * @param event ignored + */ + public void moveFocusRight(final PInputEvent event) { + moveFocusInDirection(EAST); + } + + /** + * Moves the focus in the up direction. Animating the camera accordingly. + * + * @param event ignored + */ + public void moveFocusUp(final PInputEvent event) { + moveFocusInDirection(NORTH); + } + + /** + * Moves the focus to the nearest node in the direction specified. Animating + * the camera appropriately. + * + * @param direction one of NORTH, SOUTH, EAST, WEST, IN, OUT + */ + private void moveFocusInDirection(final int direction) { + final PNode n = getNeighborInDirection(direction); + + if (n != null) { + focusNode = n; + } + } + + /** + * Moves the focus to the mouse under the mouse. Animating the camera + * appropriately. + * + * @param event mouse event + */ + public void moveFocusToMouseOver(final PInputEvent event) { + final PNode focus = event.getPickedNode(); + if (!(focus instanceof PCamera)) { + focusNode = focus; + } + } + + /** + * Returns the nearest node in the given direction. + * + * @param direction direction in which to look the nearest node + * + * @return nearest node in the given direction + */ + public PNode getNeighborInDirection(final int direction) { + if (focusNode == null) { + return null; + } + + NODE_TO_GLOBAL_NODE_CENTER_MAPPING.clear(); + + final Point2D highlightCenter = focusNode.getGlobalFullBounds().getCenter2D(); + NODE_TO_GLOBAL_NODE_CENTER_MAPPING.put(focusNode, highlightCenter); + + final List l = getNeighbors(); + sortNodesByDistanceFromPoint(l, highlightCenter); + + final Iterator i = l.iterator(); + while (i.hasNext()) { + final PNode each = (PNode) i.next(); + if (nodeIsNeighborInDirection(each, direction)) { + return each; + } + } + + return null; + } + + /** + * Returns all pickable nodes that are 1 hop away from the currently focused + * node. This includes, parent, children, and siblings. + * + * @return list of nodes that are 1 hop away from the current focusNode + */ + public List getNeighbors() { + final ArrayList result = new ArrayList(); + if (focusNode == null || focusNode.getParent() == null) { + return result; + } + + final PNode focusParent = focusNode.getParent(); + + final Iterator i = focusParent.getChildrenIterator(); + + while (i.hasNext()) { + final PNode each = (PNode) i.next(); + if (each != focusNode && each.getPickable()) { + result.add(each); + } + } + + result.add(focusParent); + result.addAll(focusNode.getChildrenReference()); + return result; + } + + /** + * Returns true if the given node is a neighbor in the given direction + * relative to the current focus. + * + * @param node the node being tested + * @param direction the direction in which we're testing + * + * @return true if node is a neighbor in the direction provided + */ + public boolean nodeIsNeighborInDirection(final PNode node, final int direction) { + switch (direction) { + case IN: + return node.isDescendentOf(focusNode); + + case OUT: + return node.isAncestorOf(focusNode); + + default: + if (node.isAncestorOf(focusNode) || node.isDescendentOf(focusNode)) { + return false; + } + } + + final Point2D highlightCenter = (Point2D) NODE_TO_GLOBAL_NODE_CENTER_MAPPING.get(focusNode); + final Point2D nodeCenter = (Point2D) NODE_TO_GLOBAL_NODE_CENTER_MAPPING.get(node); + + final double ytest1 = nodeCenter.getX() - highlightCenter.getX() + highlightCenter.getY(); + final double ytest2 = -nodeCenter.getX() + highlightCenter.getX() + highlightCenter.getY(); + + switch (direction) { + case NORTH: + return nodeCenter.getY() < highlightCenter.getY() && nodeCenter.getY() < ytest1 + && nodeCenter.getY() < ytest2; + + case EAST: + return nodeCenter.getX() > highlightCenter.getX() && nodeCenter.getY() < ytest1 + && nodeCenter.getY() > ytest2; + + case SOUTH: + return nodeCenter.getY() > highlightCenter.getY() && nodeCenter.getY() > ytest1 + && nodeCenter.getY() > ytest2; + + case WEST: + return nodeCenter.getX() < highlightCenter.getX() && nodeCenter.getY() > ytest1 + && nodeCenter.getY() < ytest2; + + default: + return false; + } + } + + /** + * Modifies the array so that it's sorted in ascending order based on the + * distance from the given point. + * + * @param nodes list of nodes to be sorted + * @param point point from which distance is being computed + */ + public void sortNodesByDistanceFromPoint(final List nodes, final Point2D point) { + Collections.sort(nodes, new Comparator() { + public int compare(final Object o1, final Object o2) { + return compare((PNode) o1, (PNode) o2); + } + + private int compare(final PNode each1, final PNode each2) { + final Point2D center1 = each1.getGlobalFullBounds().getCenter2D(); + final Point2D center2 = each2.getGlobalFullBounds().getCenter2D(); + + NODE_TO_GLOBAL_NODE_CENTER_MAPPING.put(each1, center1); + NODE_TO_GLOBAL_NODE_CENTER_MAPPING.put(each2, center2); + + return Double.compare(point.distance(center1), point.distance(center2)); + } + }); + } + + // **************************************************************** + // Canvas Movement - The canvas view is updated so that the current + // focus remains visible on the screen at 100 percent scale. + // **************************************************************** + + /** + * Animates the camera's view transform into the provided one over the + * duration provided. + * + * @param camera camera being animated + * @param targetTransform the transform to which the camera's transform will + * be animated + * @param duration the number of milliseconds the animation should last + * + * @return an activity object that represents the animation + */ + protected PActivity animateCameraViewTransformTo(final PCamera camera, final AffineTransform targetTransform, + final int duration) { + boolean wasOldAnimation = false; + + // first stop any old animations. + if (navigationActivity != null) { + navigationActivity.terminate(); + wasOldAnimation = true; + } + + if (duration == 0) { + camera.setViewTransform(targetTransform); + return null; + } + + final AffineTransform source = camera.getViewTransformReference(); + + if (source.equals(targetTransform)) { + return null; + } + + navigationActivity = camera.animateViewToTransform(targetTransform, duration); + navigationActivity.setSlowInSlowOut(!wasOldAnimation); + return navigationActivity; + } + + /** + * Animates the Camera's view so that it contains the new focus node. + * + * @param camera The camera to be animated + * @param newFocus the node that will gain focus + * @param duration number of milliseconds that animation should last for + * + * @return an activity object representing the scheduled animation + */ + public PActivity directCameraViewToFocus(final PCamera camera, final PNode newFocus, final int duration) { + focusNode = newFocus; + final AffineTransform originalViewTransform = camera.getViewTransform(); + + final PDimension d = new PDimension(1, 0); + focusNode.globalToLocal(d); + + final double scaleFactor = d.getWidth() / camera.getViewScale(); + final Point2D scalePoint = focusNode.getGlobalFullBounds().getCenter2D(); + if (Math.abs(1f - scaleFactor) < SCALING_THRESHOLD) { + camera.scaleViewAboutPoint(scaleFactor, scalePoint.getX(), scalePoint.getY()); + } + + // Pan the canvas to include the view bounds with minimal canvas + // movement. + camera.animateViewToPanToBounds(focusNode.getGlobalFullBounds(), 0); + + // Get rid of any white space. The canvas may be panned and + // zoomed in to do this. But make sure not stay constrained by max + // magnification. + // fillViewWhiteSpace(aCamera); + + final AffineTransform resultingTransform = camera.getViewTransform(); + camera.setViewTransform(originalViewTransform); + + // Animate the canvas so that it ends up with the given + // view transform. + return animateCameraViewTransformTo(camera, resultingTransform, duration); + } + + /** + * Instantaneously transforms the provided camera so that it does not + * contain any extra white space. + * + * @param camera the camera to be transformed + */ + protected void fillViewWhiteSpace(final PCamera camera) { + final PBounds rootBounds = camera.getRoot().getFullBoundsReference(); + + if (rootBounds.contains(camera.getViewBounds())) { + return; + } + + camera.animateViewToPanToBounds(rootBounds, 0); + camera.animateViewToPanToBounds(focusNode.getGlobalFullBounds(), 0); + + // center content. + double dx = 0; + double dy = 0; + + PBounds viewBounds = camera.getViewBounds(); + + if (viewBounds.getWidth() > rootBounds.getWidth()) { + // then center along x axis. + dx = rootBounds.getCenterX() - viewBounds.getCenterX(); + } + + if (viewBounds.getHeight() > rootBounds.getHeight()) { + // then center along y axis. + dy = rootBounds.getCenterX() - viewBounds.getCenterX(); + } + + camera.translateView(dx, dy); + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/event/PNotification.java b/src/main/java/edu/umd/cs/piccolox/event/PNotification.java new file mode 100644 index 0000000..2ef81f5 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/event/PNotification.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This class PNotification center is derived from the class + * NSNotification from: + * + * Wotonomy: OpenStep design patterns for pure Java + * applications. Copyright (C) 2000 Blacksmith, Inc. + */ +package edu.umd.cs.piccolox.event; + +import java.util.Map; + +/** + * PNotification objects encapsulate information so that it can be + * broadcast to other objects by a PNotificationCenter. A PNotification contains + * a name, an object, and an optional properties map. The name is a tag + * identifying the notification. The object is any object that the poster of the + * notification wants to send to observers of that notification (typically, it + * is the object that posted the notification). The properties map stores other + * related objects, if any. + *+ * You don't usually create your own notifications directly. The + * PNotificationCenter method postNotification() allow you to conveniently post + * a notification without creating it first. + *
+ * + * @author Jesse Grosjean + */ +public class PNotification { + /** Name of the notification. */ + protected String name; + /** The Object associated with this notification. */ + protected Object source; + /** A free form map of properties to attach to this notification. */ + protected Map properties; + + /** + * Creates a notification. + * + * @param name Arbitrary name of the notification + * @param source object associated with this notification + * @param properties free form map of information about the notification + */ + public PNotification(final String name, final Object source, final Map properties) { + this.name = name; + this.source = source; + this.properties = properties; + } + + /** + * Return the name of the notification. This is the same as the name used to + * register with the notification center. + * + * @return name of notification + */ + public String getName() { + return name; + } + + /** + * Return the object associated with this notification. This is most often + * the same object that posted the notification. It may be null. + * + * @return object associated with this notification + */ + public Object getObject() { + return source; + } + + /** + * Return a property associated with the notification, or null if not found. + * + * @param key key used for looking up the property + * @return value associated with the key or null if not found + */ + public Object getProperty(final Object key) { + if (properties != null) { + return properties.get(key); + } + return null; + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/event/PNotificationCenter.java b/src/main/java/edu/umd/cs/piccolox/event/PNotificationCenter.java new file mode 100644 index 0000000..5517bad --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/event/PNotificationCenter.java @@ -0,0 +1,556 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This class PNotificationCenter center is derived from the class + * NSNotificationCenter from: + * + * Wotonomy: OpenStep design patterns for pure Java + * applications. Copyright (C) 2000 Blacksmith, Inc. + */ +package edu.umd.cs.piccolox.event; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * PNotificationCenter provides a way for objects that don't know about + * each other to communicate. It receives PNotification objects and broadcasts + * them to all interested listeners. Unlike standard Java events, the event + * listeners don't need to know about the event source, and the event source + * doesn't need to maintain the list of listeners. + *+ * Listeners of the notifications center are held by weak references. So the + * notification center will not create garbage collection problems as standard + * java event listeners do. + *
+ * + * @author Jesse Grosjean + */ +public class PNotificationCenter { + /** Used as a place holder for null names or objects. */ + public static final Object NULL_MARKER = new Object(); + + /** Singleton instance of the notification center. */ + protected static volatile PNotificationCenter DEFAULT_CENTER; + + /** A map of listeners keyed by NotificationKey objects. */ + protected HashMap listenersMap; + + /** A queue of NotificationKeys that are available to be garbage collected. */ + protected ReferenceQueue keyQueue; + + /** + * Singleton accessor for the PNotificationCenter. + * + * @return singleton instance of PNotificationCenter + */ + public static PNotificationCenter defaultCenter() { + if (DEFAULT_CENTER == null) { + DEFAULT_CENTER = new PNotificationCenter(); + } + return DEFAULT_CENTER; + } + + private PNotificationCenter() { + listenersMap = new HashMap(); + keyQueue = new ReferenceQueue(); + } + + /** + * Registers the 'listener' to receive notifications with the name + * 'notificationName' and/or containing 'object'. When a matching + * notification is posted the callBackMethodName message will be sent to the + * listener with a single PNotification argument. If notificationName is + * null then the listener will receive all notifications with an object + * matching 'object'. If 'object' is null the listener will receive all + * notifications with the name 'notificationName'. + * + * @param listener object to be notified of notifications + * @param callbackMethodName method to be invoked on the listener + * @param notificationName name of notifications to filter on + * @param object source of notification messages that this listener is + * interested in + * @return true if listener has been added + */ + public boolean addListener(final Object listener, final String callbackMethodName, final String notificationName, + final Object object) { + processKeyQueue(); + + final Object name = nullify(notificationName); + final Object sanitizedObject = nullify(object); + + final Method method = extractCallbackMethod(listener, callbackMethodName); + if (method == null) { + return false; + } + + final NotificationKey key = new NotificationKey(name, sanitizedObject); + final NotificationTarget notificationTarget = new NotificationTarget(listener, method); + + List list = (List) listenersMap.get(key); + if (list == null) { + list = new ArrayList(); + listenersMap.put(new NotificationKey(name, sanitizedObject, keyQueue), list); + } + + if (!list.contains(notificationTarget)) { + list.add(notificationTarget); + } + + return true; + } + + private Method extractCallbackMethod(final Object listener, final String methodName) { + Method method = null; + try { + Class[] classes = new Class[1]; + classes[0] = PNotification.class; + method = listener.getClass().getMethod(methodName, classes); + } + catch (final NoSuchMethodException e) { + return null; + } + + final int modifiers = method.getModifiers(); + if (!Modifier.isPublic(modifiers)) { + return null; + } + + return method; + } + + /** + * Sanitizes the object reference by returning NULL_MARKER if the object is + * null. + * + * @param object object to sanitize + * + * @return NULL_MARKER is object is null, otherwise object + */ + private Object nullify(final Object object) { + if (object == null) { + return NULL_MARKER; + } + + return object; + } + + // **************************************************************** + // Remove Listener Methods + // **************************************************************** + + /** + * Removes the listener so that it no longer receives notfications from this + * notification center. + * + * @param listener listener to be removed from this notification center + */ + public void removeListener(final Object listener) { + processKeyQueue(); + + final Iterator i = new LinkedList(listenersMap.keySet()).iterator(); + while (i.hasNext()) { + removeListener(listener, i.next()); + } + } + + /** + * Unregisters the listener as a listener for the specified kind of + * notification. + * + * If listener is null all listeners matching notificationName and object + * are removed. + * + * If notificationName is null the listener will be removed from all + * notifications containing the object. + * + * If the object is null then the listener will be removed from all + * notifications matching notficationName. + * + * @param listener listener to be removed + * @param notificationName name of notifications or null for all + * @param object notification source or null for all + */ + public void removeListener(final Object listener, final String notificationName, final Object object) { + processKeyQueue(); + + final List keys = matchingKeys(notificationName, object); + final Iterator it = keys.iterator(); + while (it.hasNext()) { + removeListener(listener, it.next()); + } + } + + // **************************************************************** + // Post PNotification Methods + // **************************************************************** + + /** + * Post a new notification with notificationName and object. The object is + * typically the object posting the notification. The object may be null. + * + * @param notificationName name of notification to post + * @param object source of the notification, null signifies unknown + */ + public void postNotification(final String notificationName, final Object object) { + postNotification(notificationName, object, null); + } + + /** + * Creates a notification with the name notificationName, associates it with + * the object, and posts it to this notification center. The object is + * typically the object posting the notification. It may be null. + * + * @param notificationName name of notification being posted + * @param object source of the notification, may be null + * @param properties properties associated with the notification + */ + public void postNotification(final String notificationName, final Object object, final Map properties) { + postNotification(new PNotification(notificationName, object, properties)); + } + + /** + * Post the notification to this notification center. Most often clients + * will instead use one of this classes convenience postNotifcations + * methods. + * + * @param notification notification to be dispatched to appropriate + * listeners + */ + public void postNotification(final PNotification notification) { + final List mergedListeners = new LinkedList(); + + final Object name = notification.getName(); + final Object object = notification.getObject(); + + if (name != null && object != null) { + fillWithMatchingListeners(name, object, mergedListeners); + fillWithMatchingListeners(null, object, mergedListeners); + fillWithMatchingListeners(name, null, mergedListeners); + } + else if (name != null) { + fillWithMatchingListeners(name, null, mergedListeners); + } + else if (object != null) { + fillWithMatchingListeners(null, object, mergedListeners); + } + + fillWithMatchingListeners(null, null, mergedListeners); + + dispatchNotifications(notification, mergedListeners); + } + + /** + * Adds all listeners that are registered to receive notifications to the + * end of the list provided. + * + * @param notificationName name of the notification being emitted + * @param object source of the notification + * @param listeners list to append listeners to + */ + private void fillWithMatchingListeners(final Object notificationName, final Object object, final List listeners) { + final Object key = new NotificationKey(nullify(notificationName), nullify(object)); + final List globalListeners = (List) listenersMap.get(key); + if (globalListeners != null) { + listeners.addAll(globalListeners); + } + } + + private void dispatchNotifications(final PNotification notification, final List listeners) { + NotificationTarget listener; + final Iterator listenerIterator = listeners.iterator(); + + while (listenerIterator.hasNext()) { + listener = (NotificationTarget) listenerIterator.next(); + if (listener.get() == null) { + listenerIterator.remove(); + } + else { + notifyListener(notification, listener); + } + } + } + + private void notifyListener(final PNotification notification, final NotificationTarget listener) { + try { + Object[] objects = new Object[1]; + objects[0] = notification; + listener.getMethod().invoke(listener.get(), objects); + } + catch (final IllegalAccessException e) { + throw new RuntimeException("Impossible Situation: invoking inaccessible method on listener", e); + } + catch (final InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a list of keys with the given name and object. + * + * @param name name of key + * @param object key associated with the object + * + * @return list of matching keys + */ + protected List matchingKeys(final String name, final Object object) { + final List result = new LinkedList(); + + final NotificationKey searchKey = new NotificationKey(name, object); + final Iterator it = listenersMap.keySet().iterator(); + while (it.hasNext()) { + final NotificationKey key = (NotificationKey) it.next(); + if (searchKey.equals(key)) { + result.add(key); + } + } + + return result; + } + + /** + * Removes the given listener from receiving notifications with the given + * key. + * + * @param listener the listener being unregistered + * @param key the key that identifies the listener + */ + protected void removeListener(final Object listener, final Object key) { + if (listener == null) { + listenersMap.remove(key); + return; + } + + final List list = (List) listenersMap.get(key); + if (list == null) { + return; + } + + final Iterator it = list.iterator(); + while (it.hasNext()) { + final Object observer = ((NotificationTarget) it.next()).get(); + if (observer == null || listener == observer) { + it.remove(); + } + } + + if (list.size() == 0) { + listenersMap.remove(key); + } + } + + /** + * Iterates over available keys in the key queue and removes the queue from + * the listener map. + */ + protected void processKeyQueue() { + NotificationKey key; + while ((key = (NotificationKey) keyQueue.poll()) != null) { + listenersMap.remove(key); + } + } + + /** + * Represents a notification type from a particular object. + */ + protected static class NotificationKey extends WeakReference { + private final Object name; + private final int hashCode; + + /** + * Creates a notification key with the provided name associated to the + * object given. + * + * @param name name of notification + * @param object associated object + */ + public NotificationKey(final Object name, final Object object) { + super(object); + this.name = name; + hashCode = name.hashCode() + object.hashCode(); + } + + /** + * Creates a notification key with the provided name associated with the + * provided object. + * + * @param name name of notification + * @param object associated object + * @param queue ReferenceQueue in which this NotificationKey will be + * appended once it has been cleared to be garbage collected + */ + public NotificationKey(final Object name, final Object object, final ReferenceQueue queue) { + super(object, queue); + this.name = name; + hashCode = name.hashCode() + object.hashCode(); + } + + /** + * Returns name of notification this key represents. + * + * @return name of notification + */ + public Object name() { + return name; + } + + /** {@inheritDoc} */ + public int hashCode() { + return hashCode; + } + + /** + * Two keys are equal if they have the same name and are associated with + * the same object and conform to all other equals rules. + * + * @param anObject object being tested for equivalence to this + * NotificationKey + * + * @return true if this object is logically equivalent to the one passed + * in + */ + public boolean equals(final Object anObject) { + if (this == anObject) { + return true; + } + + if (!(anObject instanceof NotificationKey)) { + return false; + } + + final NotificationKey key = (NotificationKey) anObject; + + if (name != key.name && (name == null || !name.equals(key.name))) { + return false; + } + + final Object object = get(); + + return object != null && object == key.get(); + } + + /** + * Returns a nice string representation of this notification key. + * + * @return string representation of this notification key + */ + public String toString() { + return "[CompoundKey:" + name() + ":" + get() + "]"; + } + } + + /** + * A NotificationTarget is a method on a particular object that can be + * invoked. + */ + protected static class NotificationTarget extends WeakReference { + /** Cached hashcode value computed at construction time. */ + protected int hashCode; + + /** Method to be invoked on the object. */ + protected Method method; + + /** + * Creates a notification target representing the method on the + * particular object provided. + * + * @param object object on which method can be invoked + * @param method method to be invoked + */ + public NotificationTarget(final Object object, final Method method) { + super(object); + hashCode = object.hashCode() + method.hashCode(); + this.method = method; + } + + /** + * Returns the method that will be invoked on the listener object. + * + * @return method to be invoked with notification is to be dispatched + */ + public Method getMethod() { + return method; + } + + /** + * Returns hash code for this notification target. + * + * @return hash code + */ + public int hashCode() { + return hashCode; + } + + /** + * Returns true if this object is logically equivalent to the one passed + * in. For this to happen they must have the same method and object. + * + * @param object object being tested for logical equivalency to this one + * + * @return true if logically equivalent + */ + public boolean equals(final Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof NotificationTarget)) { + return false; + } + + final NotificationTarget target = (NotificationTarget) object; + if (method != target.method && (method == null || !method.equals(target.method))) { + return false; + } + + final Object o = get(); + + return o != null && o == target.get(); + } + + /** + * Returns a string representation of this NotificationTarget for + * debugging purposes. + * + * @return string representation + */ + public String toString() { + return "[CompoundValue:" + get() + ":" + getMethod().getName() + "]"; + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/event/PSelectionEventHandler.java b/src/main/java/edu/umd/cs/piccolox/event/PSelectionEventHandler.java new file mode 100644 index 0000000..c9f4b0e --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/event/PSelectionEventHandler.java @@ -0,0 +1,919 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.event; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Paint; +import java.awt.Stroke; +import java.awt.event.KeyEvent; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PLayer; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.event.PDragSequenceEventHandler; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.nodes.PPath; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PDimension; +import edu.umd.cs.piccolo.util.PNodeFilter; +import edu.umd.cs.piccolox.handles.PBoundsHandle; + +/** + *PSelectionEventHandler
provides standard interaction for
+ * selection. Clicking selects the object under the cursor. Shift-clicking
+ * allows multiple objects to be selected. Dragging offers marquee selection.
+ * Pressing the delete key deletes the selection by default.
+ *
+ * @version 1.0
+ * @author Ben Bederson
+ */
+public class PSelectionEventHandler extends PDragSequenceEventHandler {
+ /**
+ * Notification name that identifies a change in the selection. Used with
+ * PNotificationCenter.
+ */
+ public static final String SELECTION_CHANGED_NOTIFICATION = "SELECTION_CHANGED_NOTIFICATION";
+
+ /** The default dash width when displaying selection rectangle. */
+ static final int DASH_WIDTH = 5;
+
+ static final int NUM_STROKES = 10;
+
+ /** The current selection. */
+ private HashMap selection = null;
+ /** List of nodes whose children can be selected. */
+ private List selectableParents = null;
+
+ private PPath marquee = null;
+ /** Node that marquee is added to as a child. */
+ private PNode marqueeParent = null;
+
+ private Point2D presspt = null;
+ private Point2D canvasPressPt = null;
+ private float strokeNum = 0;
+ private Stroke[] strokes = null;
+
+ /** Used within drag handler temporarily. */
+ private HashMap allItems = null;
+
+ /** Used within drag handler temporarily. */
+ private ArrayList unselectList = null;
+ private HashMap marqueeMap = null;
+
+ /** Node pressed on (or null if none). */
+ private PNode pressNode = null;
+
+ /** True if DELETE key should delete selection. */
+ private boolean deleteKeyActive = true;
+
+ /** Paint applied when drawing the marquee. */
+ private Paint marqueePaint;
+
+ /** How transparent the marquee should be. */
+ private float marqueePaintTransparency = 1.0f;
+
+ /**
+ * Creates a selection event handler.
+ *
+ * @param marqueeParent The node to which the event handler dynamically adds
+ * a marquee (temporarily) to represent the area being selected.
+ * @param selectableParent The node whose children will be selected by this
+ * event handler.
+ */
+ public PSelectionEventHandler(final PNode marqueeParent, final PNode selectableParent) {
+ this.marqueeParent = marqueeParent;
+ selectableParents = new ArrayList();
+ selectableParents.add(selectableParent);
+ init();
+ }
+
+ /**
+ * Creates a selection event handler.
+ *
+ * @param marqueeParent The node to which the event handler dynamically adds
+ * a marquee (temporarily) to represent the area being selected.
+ * @param selectableParents A list of nodes whose children will be selected
+ * by this event handler.
+ */
+ public PSelectionEventHandler(final PNode marqueeParent, final List selectableParents) {
+ this.marqueeParent = marqueeParent;
+ this.selectableParents = selectableParents;
+ init();
+ }
+
+ /**
+ * Initializes the PSelectionEventHandler with a marquee stroke.
+ */
+ protected void init() {
+ final float[] dash = new float[2];
+ dash[0] = DASH_WIDTH;
+ dash[1] = DASH_WIDTH;
+
+ strokes = new Stroke[NUM_STROKES];
+ for (int i = 0; i < NUM_STROKES; i++) {
+ strokes[i] = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, dash, i);
+ }
+
+ selection = new HashMap();
+ allItems = new HashMap();
+ unselectList = new ArrayList();
+ marqueeMap = new HashMap();
+ }
+
+ /**
+ * Marks all items as selected.
+ *
+ * @param items collection of items to be selected
+ */
+ public void select(final Collection items) {
+ boolean changes = false;
+ final Iterator itemIt = items.iterator();
+ while (itemIt.hasNext()) {
+ final PNode node = (PNode) itemIt.next();
+ changes |= internalSelect(node);
+ }
+ if (changes) {
+ postSelectionChanged();
+ }
+ }
+
+ /**
+ * Marks all keys as selected.
+ *
+ * @param items map where keys are to be selected
+ */
+ public void select(final Map items) {
+ select(items.keySet());
+ }
+
+ /**
+ * Select the passed node if not already selected.
+ *
+ * @param node node to be selected
+ * @return true if node was not already selected
+ */
+ private boolean internalSelect(final PNode node) {
+ if (isSelected(node)) {
+ return false;
+ }
+
+ selection.put(node, Boolean.TRUE);
+ decorateSelectedNode(node);
+ return true;
+ }
+
+ /**
+ * Dispatches a selection changed notification to the PNodificationCenter.
+ */
+ private void postSelectionChanged() {
+ PNotificationCenter.defaultCenter().postNotification(SELECTION_CHANGED_NOTIFICATION, this);
+ }
+
+ /**
+ * Selected the provided node if not already selected.
+ *
+ * @param node node to be selected
+ */
+ public void select(final PNode node) {
+ if (internalSelect(node)) {
+ postSelectionChanged();
+ }
+ }
+
+ /**
+ * Adds bound handles to the provided node.
+ *
+ * @param node node to be decorated
+ */
+ public void decorateSelectedNode(final PNode node) {
+ PBoundsHandle.addBoundsHandlesTo(node);
+ }
+
+ /**
+ * Removes all nodes provided from the selection.
+ *
+ * @param items items to remove form the selection
+ */
+ public void unselect(final Collection items) {
+ boolean changes = false;
+ final Iterator itemIt = items.iterator();
+ while (itemIt.hasNext()) {
+ final PNode node = (PNode) itemIt.next();
+ changes |= internalUnselect(node);
+ }
+ if (changes) {
+ postSelectionChanged();
+ }
+ }
+
+ /**
+ * Removes provided selection node if not already selected.
+ *
+ * @param node node to remove from selection
+ *
+ * @return true on success
+ */
+ private boolean internalUnselect(final PNode node) {
+ if (!isSelected(node)) {
+ return false;
+ }
+
+ undecorateSelectedNode(node);
+ selection.remove(node);
+ return true;
+ }
+
+ /**
+ * Removes node from selection.
+ *
+ * @param node node to be removed from selection
+ */
+ public void unselect(final PNode node) {
+ if (internalUnselect(node)) {
+ postSelectionChanged();
+ }
+ }
+
+ /**
+ * Removes bounds handles from node.
+ *
+ * @param node to have handles removed from
+ */
+ public void undecorateSelectedNode(final PNode node) {
+ PBoundsHandle.removeBoundsHandlesFrom(node);
+ }
+
+ /**
+ * Empties the selection.
+ */
+ public void unselectAll() {
+ // Because unselect() removes from selection, we need to
+ // take a copy of it first so it isn't changed while we're iterating
+ final ArrayList sel = new ArrayList(selection.keySet());
+ unselect(sel);
+ }
+
+ /**
+ * Returns true is provided node is selected.
+ *
+ * @param node - node to be tested
+ * @return true if succeeded
+ */
+ public boolean isSelected(final PNode node) {
+ return (node != null && selection.containsKey(node));
+ }
+
+ /**
+ * Returns a copy of the currently selected nodes.
+ *
+ * @return copy of selection
+ */
+ public Collection getSelection() {
+ return new ArrayList(selection.keySet());
+ }
+
+ /**
+ * Gets a reference to the currently selected nodes. You should not modify
+ * or store this collection.
+ *
+ * @return direct reference to selection
+ */
+ public Collection getSelectionReference() {
+ return Collections.unmodifiableCollection(selection.keySet());
+ }
+
+ /**
+ * Determine if the specified node can be selected (i.e., if it is a child
+ * of the one the list of nodes that can be selected).
+ *
+ * @param node node being tested
+ * @return true if node can be selected
+ */
+ protected boolean isSelectable(final PNode node) {
+ boolean selectable = false;
+
+ final Iterator parentsIt = selectableParents.iterator();
+ while (parentsIt.hasNext()) {
+ final PNode parent = (PNode) parentsIt.next();
+ if (parent.getChildrenReference().contains(node)) {
+ selectable = true;
+ break;
+ }
+ else if (parent instanceof PCamera) {
+ for (int i = 0; i < ((PCamera) parent).getLayerCount(); i++) {
+ final PLayer layer = ((PCamera) parent).getLayer(i);
+ if (layer.getChildrenReference().contains(node)) {
+ selectable = true;
+ break;
+ }
+ }
+ }
+ }
+
+ return selectable;
+ }
+
+ /**
+ * Flags the node provided as a selectable parent. This makes it possible to
+ * select its children.
+ *
+ * @param node to flag as selectable
+ */
+ public void addSelectableParent(final PNode node) {
+ selectableParents.add(node);
+ }
+
+ /**
+ * Removes the node provided from the set of selectable parents. This makes
+ * its impossible to select its children.
+ *
+ * @param node to remove from selectable parents
+ */
+ public void removeSelectableParent(final PNode node) {
+ selectableParents.remove(node);
+ }
+
+ /**
+ * Sets the node provided as the *only* selectable parent.
+ *
+ * @param node node to become the 1 and only selectable parent
+ */
+ public void setSelectableParent(final PNode node) {
+ selectableParents.clear();
+ selectableParents.add(node);
+ }
+
+ /**
+ * Sets the collection of selectable parents as the only parents that are
+ * selectable.
+ *
+ * @param c nodes to become selectable parents.
+ */
+ public void setSelectableParents(final Collection c) {
+ selectableParents.clear();
+ selectableParents.addAll(c);
+ }
+
+ /**
+ * Returns all selectable parents.
+ *
+ * @return selectable parents
+ */
+ public Collection getSelectableParents() {
+ return new ArrayList(selectableParents);
+ }
+
+ // //////////////////////////////////////////////////////
+ // The overridden methods from PDragSequenceEventHandler
+ // //////////////////////////////////////////////////////
+
+ /**
+ * Overrides method in PDragSequenceEventHandler so that, selections have
+ * marquees.
+ *
+ * @param e the event that started the drag
+ */
+ protected void startDrag(final PInputEvent e) {
+ super.startDrag(e);
+
+ initializeSelection(e);
+
+ if (isMarqueeSelection(e)) {
+ initializeMarquee(e);
+
+ if (!isOptionSelection(e)) {
+ startMarqueeSelection(e);
+ }
+ else {
+ startOptionMarqueeSelection(e);
+ }
+ }
+ else {
+ if (!isOptionSelection(e)) {
+ startStandardSelection(e);
+ }
+ else {
+ startStandardOptionSelection(e);
+ }
+ }
+ }
+
+ /**
+ * Updates the marquee to the new bounds caused by the drag.
+ *
+ * @param event drag event
+ */
+ protected void drag(final PInputEvent event) {
+ super.drag(event);
+
+ if (isMarqueeSelection(event)) {
+ updateMarquee(event);
+
+ if (!isOptionSelection(event)) {
+ computeMarqueeSelection(event);
+ }
+ else {
+ computeOptionMarqueeSelection(event);
+ }
+ }
+ else {
+ dragStandardSelection(event);
+ }
+ }
+
+ /**
+ * Ends the selection marquee when the drag is ended.
+ *
+ * @param event the event responsible for ending the drag
+ */
+ protected void endDrag(final PInputEvent event) {
+ super.endDrag(event);
+
+ if (isMarqueeSelection(event)) {
+ endMarqueeSelection(event);
+ }
+ else {
+ endStandardSelection(event);
+ }
+ }
+
+ // //////////////////////////
+ // Additional methods
+ // //////////////////////////
+
+ /**
+ * Used to test whether the event is one that changes the selection.
+ *
+ * @param pie The event under test
+ * @return true if event changes the selection
+ */
+ public boolean isOptionSelection(final PInputEvent pie) {
+ return pie.isShiftDown();
+ }
+
+ /**
+ * Tests the input event to see if it is selecting a new node.
+ *
+ * @param pie event under test
+ * @return true if there is no current selection
+ */
+ protected boolean isMarqueeSelection(final PInputEvent pie) {
+ return pressNode == null;
+ }
+
+ /**
+ * Starts a selection based on the provided event.
+ *
+ * @param pie event used to populate the selection
+ */
+ protected void initializeSelection(final PInputEvent pie) {
+ canvasPressPt = pie.getCanvasPosition();
+ presspt = pie.getPosition();
+ pressNode = pie.getPath().getPickedNode();
+ if (pressNode instanceof PCamera) {
+ pressNode = null;
+ }
+ }
+
+ /**
+ * Creates an empty marquee child for use in displaying the marquee around
+ * the selection.
+ *
+ * @param event event responsible for the initialization
+ */
+ protected void initializeMarquee(final PInputEvent event) {
+ marquee = PPath.createRectangle((float) presspt.getX(), (float) presspt.getY(), 0, 0);
+ marquee.setPaint(marqueePaint);
+ marquee.setTransparency(marqueePaintTransparency);
+ marquee.setStrokePaint(Color.black);
+ marquee.setStroke(strokes[0]);
+ marqueeParent.addChild(marquee);
+
+ marqueeMap.clear();
+ }
+
+ /**
+ * Invoked when the marquee is being used to extend the selection.
+ *
+ * @param event event causing the option selection
+ */
+ protected void startOptionMarqueeSelection(final PInputEvent event) {
+ }
+
+ /**
+ * Invoked at the start of the selection. Removes any selections.
+ *
+ * @param event event causing a new marquee selection
+ */
+ protected void startMarqueeSelection(final PInputEvent event) {
+ unselectAll();
+ }
+
+ /**
+ * If the pressed node is not selected unselect all nodes and select the
+ * pressed node if it allows it.
+ *
+ * @param pie event that started the selection
+ */
+ protected void startStandardSelection(final PInputEvent pie) {
+ // Option indicator not down - clear selection, and start fresh
+ if (isSelected(pressNode)) {
+ return;
+ }
+
+ unselectAll();
+
+ if (isSelectable(pressNode)) {
+ select(pressNode);
+ }
+ }
+
+ /**
+ * Toggle the current selection on the node that was just pressed, but leave
+ * the rest of the selected nodes unchanged.
+ *
+ * @param pie event responsible for the change in selection
+ */
+ protected void startStandardOptionSelection(final PInputEvent pie) {
+ if (isSelectable(pressNode)) {
+ if (isSelected(pressNode)) {
+ unselect(pressNode);
+ }
+ else {
+ select(pressNode);
+ }
+ }
+ }
+
+ /**
+ * Updates the marquee rectangle as the result of a drag.
+ *
+ * @param pie event responsible for the change in the marquee
+ */
+ protected void updateMarquee(final PInputEvent pie) {
+ final PBounds b = new PBounds();
+
+ if (marqueeParent instanceof PCamera) {
+ b.add(canvasPressPt);
+ b.add(pie.getCanvasPosition());
+ }
+ else {
+ b.add(presspt);
+ b.add(pie.getPosition());
+ }
+
+ marquee.globalToLocal(b);
+ marquee.setPathToRectangle((float) b.x, (float) b.y, (float) b.width, (float) b.height);
+ b.reset();
+ b.add(presspt);
+ b.add(pie.getPosition());
+
+ allItems.clear();
+ final PNodeFilter filter = createNodeFilter(b);
+ final Iterator parentsIt = selectableParents.iterator();
+ while (parentsIt.hasNext()) {
+ final PNode parent = (PNode) parentsIt.next();
+
+ Collection items;
+ if (parent instanceof PCamera) {
+ items = new ArrayList();
+ for (int i = 0; i < ((PCamera) parent).getLayerCount(); i++) {
+ ((PCamera) parent).getLayer(i).getAllNodes(filter, items);
+ }
+ }
+ else {
+ items = parent.getAllNodes(filter, null);
+ }
+
+ final Iterator itemsIt = items.iterator();
+ while (itemsIt.hasNext()) {
+ allItems.put(itemsIt.next(), Boolean.TRUE);
+ }
+ }
+ }
+
+ /**
+ * Sets the selection to be all nodes under the marquee.
+ *
+ * @param pie event responsible for the new selection
+ */
+ protected void computeMarqueeSelection(final PInputEvent pie) {
+ unselectList.clear();
+ // Make just the items in the list selected
+ // Do this efficiently by first unselecting things not in the list
+ Iterator selectionEn = selection.keySet().iterator();
+ while (selectionEn.hasNext()) {
+ final PNode node = (PNode) selectionEn.next();
+ if (!allItems.containsKey(node)) {
+ unselectList.add(node);
+ }
+ }
+ unselect(unselectList);
+
+ // Then select the rest
+ selectionEn = allItems.keySet().iterator();
+ while (selectionEn.hasNext()) {
+ final PNode node = (PNode) selectionEn.next();
+ if (!selection.containsKey(node) && !marqueeMap.containsKey(node) && isSelectable(node)) {
+ marqueeMap.put(node, Boolean.TRUE);
+ }
+ else if (!isSelectable(node)) {
+ selectionEn.remove();
+ }
+ }
+
+ select(allItems);
+ }
+
+ /**
+ * Extends the selection to include all nodes under the marquee.
+ *
+ * @param pie event responsible for the change in selection
+ */
+ protected void computeOptionMarqueeSelection(final PInputEvent pie) {
+ unselectList.clear();
+ Iterator selectionEn = selection.keySet().iterator();
+ while (selectionEn.hasNext()) {
+ final PNode node = (PNode) selectionEn.next();
+ if (!allItems.containsKey(node) && marqueeMap.containsKey(node)) {
+ marqueeMap.remove(node);
+ unselectList.add(node);
+ }
+ }
+ unselect(unselectList);
+
+ // Then select the rest
+ selectionEn = allItems.keySet().iterator();
+ while (selectionEn.hasNext()) {
+ final PNode node = (PNode) selectionEn.next();
+ if (!selection.containsKey(node) && !marqueeMap.containsKey(node) && isSelectable(node)) {
+ marqueeMap.put(node, Boolean.TRUE);
+ }
+ else if (!isSelectable(node)) {
+ selectionEn.remove();
+ }
+ }
+
+ select(allItems);
+ }
+
+ /**
+ * Creates a node filter that will filter all nodes not touching the bounds
+ * provided.
+ *
+ * @param bounds will be used to filter matches
+ *
+ * @return newly created filter
+ */
+ protected PNodeFilter createNodeFilter(final PBounds bounds) {
+ return new BoundsFilter(bounds);
+ }
+
+ /**
+ * Returns the bounds of the current selection marquee.
+ *
+ * @return bounds of current selection marquee
+ */
+ protected PBounds getMarqueeBounds() {
+ if (marquee != null) {
+ return marquee.getBounds();
+ }
+ return new PBounds();
+ }
+
+ /**
+ * Drag selected nodes.
+ *
+ * @param e event responsible for the drag
+ */
+ protected void dragStandardSelection(final PInputEvent e) {
+ // There was a press node, so drag selection
+ final PDimension d = e.getCanvasDelta();
+ e.getTopCamera().localToView(d);
+
+ final PDimension gDist = new PDimension();
+ final Iterator selectionEn = getSelection().iterator();
+ while (selectionEn.hasNext()) {
+ final PNode node = (PNode) selectionEn.next();
+
+ gDist.setSize(d);
+ node.getParent().globalToLocal(gDist);
+ node.offset(gDist.getWidth(), gDist.getHeight());
+ }
+ }
+
+ /**
+ * Removes marquee and clears selection.
+ *
+ * @param e event responsible for the end of the selection
+ */
+ protected void endMarqueeSelection(final PInputEvent e) {
+ // Remove marquee
+ allItems.clear();
+ marqueeMap.clear();
+ marquee.removeFromParent();
+ marquee = null;
+ }
+
+ /**
+ * Ends the "pressed" state of the previously pressed node (if any).
+ *
+ * @param e event responsible for the end in the selection
+ */
+ protected void endStandardSelection(final PInputEvent e) {
+ pressNode = null;
+ }
+
+ /**
+ * This gets called continuously during the drag, and is used to animate the
+ * marquee.
+ *
+ * @param aEvent event responsible for this step in the drag sequence
+ */
+ protected void dragActivityStep(final PInputEvent aEvent) {
+ if (marquee != null) {
+ final float origStrokeNum = strokeNum;
+ strokeNum = (strokeNum + 0.5f) % NUM_STROKES; // Increment by
+ // partial steps to
+ // slow down animation
+ if ((int) strokeNum != (int) origStrokeNum) {
+ marquee.setStroke(strokes[(int) strokeNum]);
+ }
+ }
+ }
+
+ /**
+ * Delete selection when delete key is pressed (if enabled).
+ *
+ * @param e the key press event
+ */
+ public void keyPressed(final PInputEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_DELETE && deleteKeyActive) {
+ final Iterator selectionEn = selection.keySet().iterator();
+ while (selectionEn.hasNext()) {
+ final PNode node = (PNode) selectionEn.next();
+ node.removeFromParent();
+ }
+ selection.clear();
+ }
+ }
+
+ /**
+ * Returns whether the delete key is a supported action.
+ *
+ * @return true if delete is allowed
+ */
+ public boolean getSupportDeleteKey() {
+ return deleteKeyActive;
+ }
+
+ /**
+ * Returns whether the delete key is a supported action.
+ *
+ * @return true if delete is allowed
+ */
+ public boolean isDeleteKeyActive() {
+ return deleteKeyActive;
+ }
+
+ /**
+ * Specifies if the DELETE key should delete the selection.
+ *
+ * @param deleteKeyActive state to set for the delete action true = enabled
+ */
+ public void setDeleteKeyActive(final boolean deleteKeyActive) {
+ this.deleteKeyActive = deleteKeyActive;
+ }
+
+ /**
+ * Class used to filter nodes that intersect with the marquee's bounds.
+ */
+ protected class BoundsFilter implements PNodeFilter {
+ private final PBounds localBounds = new PBounds();
+ private final PBounds bounds;
+
+ /**
+ * Constructs a BoundsFilter for the given bounds.
+ *
+ * @param bounds bounds to be used when testing nodes for intersection
+ */
+ protected BoundsFilter(final PBounds bounds) {
+ this.bounds = bounds;
+ }
+
+ /**
+ * Returns true if the node is an acceptable selection.
+ *
+ * @param node node being tested
+ * @return true if node is an acceptable selection
+ */
+ public boolean accept(final PNode node) {
+ localBounds.setRect(bounds);
+ node.globalToLocal(localBounds);
+
+ final boolean boundsIntersects = node.intersects(localBounds);
+ final boolean isMarquee = node == marquee;
+ return node.getPickable() && boundsIntersects && !isMarquee && !selectableParents.contains(node)
+ && !isCameraLayer(node);
+ }
+
+ /**
+ * Returns whether this filter should accept all children of a node.
+ *
+ * @param node node being tested
+ * @return true if selection should accept children children of the node
+ */
+ public boolean acceptChildrenOf(final PNode node) {
+ return selectableParents.contains(node) || isCameraLayer(node);
+ }
+
+ /**
+ * Tests a node to see if it's a layer that has an attached camera.
+ *
+ * @param node node being tested
+ * @return true if node is a layer with a camera attached
+ */
+ public boolean isCameraLayer(final PNode node) {
+ if (node instanceof PLayer) {
+ for (final Iterator i = selectableParents.iterator(); i.hasNext();) {
+ final PNode parent = (PNode) i.next();
+ if (parent instanceof PCamera && ((PCamera) parent).indexOfLayer((PLayer) node) != -1) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Indicates the color used to paint the marquee.
+ *
+ * @return the paint for interior of the marquee
+ */
+ public Paint getMarqueePaint() {
+ return marqueePaint;
+ }
+
+ /**
+ * Sets the color used to paint the marquee.
+ *
+ * @param paint the paint color
+ */
+ public void setMarqueePaint(final Paint paint) {
+ marqueePaint = paint;
+ }
+
+ /**
+ * Indicates the transparency level for the interior of the marquee.
+ *
+ * @return Returns the marquee paint transparency, zero to one
+ */
+ public float getMarqueePaintTransparency() {
+ return marqueePaintTransparency;
+ }
+
+ /**
+ * Sets the transparency level for the interior of the marquee.
+ *
+ * @param marqueePaintTransparency The marquee paint transparency to set.
+ */
+ public void setMarqueePaintTransparency(final float marqueePaintTransparency) {
+ this.marqueePaintTransparency = marqueePaintTransparency;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolox/event/PStyledTextEventHandler.java b/src/main/java/edu/umd/cs/piccolox/event/PStyledTextEventHandler.java
new file mode 100644
index 0000000..196df05
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/event/PStyledTextEventHandler.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.event;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Insets;
+import java.awt.RenderingHints;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+
+import javax.swing.JTextPane;
+import javax.swing.SwingUtilities;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.LineBorder;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.Document;
+import javax.swing.text.JTextComponent;
+import javax.swing.text.SimpleAttributeSet;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyledDocument;
+
+import edu.umd.cs.piccolo.PCamera;
+import edu.umd.cs.piccolo.PCanvas;
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
+import edu.umd.cs.piccolo.event.PInputEvent;
+import edu.umd.cs.piccolo.event.PInputEventFilter;
+import edu.umd.cs.piccolox.nodes.PStyledText;
+
+/**
+ * @author Lance Good
+ */
+public class PStyledTextEventHandler extends PBasicInputEventHandler {
+ private static final int TEXT_EDIT_PADDING = 3;
+
+ /** Canvas onto which this event handler is attached. */
+ protected PCanvas canvas;
+
+ /** Editor used to edit a PStyledText's content when it is in edit mode. */
+ protected JTextComponent editor;
+
+ /**
+ * A listener that will handle programatic changes to the underlying
+ * document and update the view accordingly.
+ */
+ protected DocumentListener docListener;
+
+ /** The Styled text being edited. */
+ protected PStyledText editedText;
+
+ /**
+ * Basic constructor for PStyledTextEventHandler.
+ *
+ * @param canvas canvas to which this handler will be attached
+ */
+ public PStyledTextEventHandler(final PCanvas canvas) {
+ final PInputEventFilter filter = new PInputEventFilter();
+ filter.setOrMask(InputEvent.BUTTON1_MASK | InputEvent.BUTTON3_MASK);
+ setEventFilter(filter);
+ this.canvas = canvas;
+ initEditor(createDefaultEditor());
+ }
+
+ /**
+ * Constructor for PStyledTextEventHandler that allows an editor to be
+ * specified.
+ *
+ * @param canvas canvas to which this handler will be attached
+ * @param editor component to display when editing a PStyledText node
+ */
+ public PStyledTextEventHandler(final PCanvas canvas, final JTextComponent editor) {
+ super();
+
+ this.canvas = canvas;
+ initEditor(editor);
+ }
+
+ /**
+ * Installs the editor onto the canvas. Making it the editor that will be
+ * used whenever a PStyledText node needs editing.
+ *
+ * @param newEditor component responsible for a PStyledText node while it is
+ * being edited.
+ */
+ protected void initEditor(final JTextComponent newEditor) {
+ editor = newEditor;
+
+ canvas.setLayout(null);
+ canvas.add(editor);
+ editor.setVisible(false);
+
+ docListener = createDocumentListener();
+ }
+
+ /**
+ * Creates a default editor component to be used when editing a PStyledText
+ * node.
+ *
+ * @return a freshly created JTextComponent subclass that can be used to
+ * edit PStyledText nodes
+ */
+ protected JTextComponent createDefaultEditor() {
+ return new DefaultTextEditor();
+ }
+
+ /**
+ * Returns a document listener that will reshape the editor whenever a
+ * change occurs to its attached document.
+ *
+ * @return a DocumentListener
+ */
+ protected DocumentListener createDocumentListener() {
+ return new DocumentListener() {
+ public void removeUpdate(final DocumentEvent e) {
+ reshapeEditorLater();
+ }
+
+ public void insertUpdate(final DocumentEvent e) {
+ reshapeEditorLater();
+ }
+
+ public void changedUpdate(final DocumentEvent e) {
+ reshapeEditorLater();
+ }
+ };
+ }
+
+ /**
+ * Creates a PStyledText instance and attaches a simple document to it. If
+ * possible, it configures its font information too.
+ *
+ * @return a new PStyledText instance
+ */
+ public PStyledText createText() {
+ final PStyledText newText = new PStyledText();
+
+ final Document doc = editor.getUI().getEditorKit(editor).createDefaultDocument();
+ if (doc instanceof StyledDocument && missingFontFamilyOrSize(doc)) {
+ final Font eFont = editor.getFont();
+ final SimpleAttributeSet sas = new SimpleAttributeSet();
+ sas.addAttribute(StyleConstants.FontFamily, eFont.getFamily());
+ sas.addAttribute(StyleConstants.FontSize, new Integer(eFont.getSize()));
+
+ ((StyledDocument) doc).setParagraphAttributes(0, doc.getLength(), sas, false);
+ }
+ newText.setDocument(doc);
+
+ return newText;
+ }
+
+ private boolean missingFontFamilyOrSize(final Document doc) {
+ return !doc.getDefaultRootElement().getAttributes().isDefined(StyleConstants.FontFamily)
+ || !doc.getDefaultRootElement().getAttributes().isDefined(StyleConstants.FontSize);
+ }
+
+ /**
+ * A callback that is invoked any time the mouse is pressed on the canvas.
+ * If the press occurs directly on the canvas, it create a new PStyledText
+ * instance and puts it in editing mode. If the click is on a node, it marks
+ * changes it to editing mode.
+ *
+ * @param event mouse click event that can be queried
+ */
+ public void mousePressed(final PInputEvent event) {
+ final PNode pickedNode = event.getPickedNode();
+
+ stopEditing(event);
+
+ if (event.getButton() != MouseEvent.BUTTON1) {
+ return;
+ }
+
+ if (pickedNode instanceof PStyledText) {
+ startEditing(event, (PStyledText) pickedNode);
+ }
+ else if (pickedNode instanceof PCamera) {
+ final PStyledText newText = createText();
+ final Insets pInsets = newText.getInsets();
+ newText.translate(event.getPosition().getX() - pInsets.left, event.getPosition().getY() - pInsets.top);
+ startEditing(event, newText);
+ }
+ }
+
+ /**
+ * Begins editing the provided text node as a result of the provided event.
+ * Will swap out the text node for an editor.
+ *
+ * @param event the event responsible for starting the editing
+ * @param text text node being edited
+ */
+ public void startEditing(final PInputEvent event, final PStyledText text) {
+ // Get the node's top right hand corner
+ final Insets pInsets = text.getInsets();
+ final Point2D nodePt = new Point2D.Double(text.getX() + pInsets.left, text.getY() + pInsets.top);
+ text.localToGlobal(nodePt);
+ event.getTopCamera().viewToLocal(nodePt);
+
+ // Update the editor to edit the specified node
+ editor.setDocument(text.getDocument());
+ editor.setVisible(true);
+
+ final Insets bInsets = editor.getBorder().getBorderInsets(editor);
+ editor.setLocation((int) nodePt.getX() - bInsets.left, (int) nodePt.getY() - bInsets.top);
+ reshapeEditorLater();
+
+ dispatchEventToEditor(event);
+ canvas.repaint();
+
+ text.setEditing(true);
+ text.getDocument().addDocumentListener(docListener);
+ editedText = text;
+ }
+
+ /**
+ * Stops editing the current text node.
+ *
+ * @param event the event responsible for stopping the editing
+ */
+ public void stopEditing(final PInputEvent event) {
+ if (editedText == null) {
+ return;
+ }
+
+ editedText.getDocument().removeDocumentListener(docListener);
+ editedText.setEditing(false);
+
+ if (editedText.getDocument().getLength() == 0) {
+ editedText.removeFromParent();
+ }
+ else {
+ editedText.syncWithDocument();
+ }
+
+ if (editedText.getParent() == null) {
+ editedText.setScale(1.0 / event.getCamera().getViewScale());
+ canvas.getLayer().addChild(editedText);
+ }
+ editor.setVisible(false);
+ canvas.repaint();
+
+ editedText = null;
+ }
+
+ /**
+ * Intercepts Piccolo2D events and dispatches the underlying swing one to
+ * the current editor.
+ *
+ * @param event the swing event being intercepted
+ */
+ public void dispatchEventToEditor(final PInputEvent event) {
+ // We have to nest the mouse press in two invoke laters so that it is
+ // fired so that the component has been completely validated at the new
+ // size and the mouse event has the correct offset
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ final MouseEvent me = new MouseEvent(editor, MouseEvent.MOUSE_PRESSED, event.getWhen(), event
+ .getModifiers()
+ | InputEvent.BUTTON1_MASK, (int) (event.getCanvasPosition().getX() - editor.getX()),
+ (int) (event.getCanvasPosition().getY() - editor.getY()), 1, false);
+ editor.dispatchEvent(me);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Adjusts the shape of the editor to fit the current document.
+ */
+ public void reshapeEditor() {
+ if (editedText != null) {
+ Dimension prefSize = editor.getPreferredSize();
+
+ final Insets textInsets = editedText.getInsets();
+ final Insets editorInsets = editor.getInsets();
+
+ final int width;
+ if (editedText.getConstrainWidthToTextWidth()) {
+ width = (int) prefSize.getWidth();
+ }
+ else {
+ width = (int) (editedText.getWidth() - textInsets.left - textInsets.right + editorInsets.left
+ + editorInsets.right + TEXT_EDIT_PADDING);
+ }
+ prefSize.setSize(width, prefSize.getHeight());
+ editor.setSize(prefSize);
+
+ prefSize = editor.getPreferredSize();
+ final int height;
+ if (editedText.getConstrainHeightToTextHeight()) {
+ height = (int) prefSize.getHeight();
+ }
+ else {
+ height = (int) (editedText.getHeight() - textInsets.top - textInsets.bottom + editorInsets.top
+ + editorInsets.bottom + TEXT_EDIT_PADDING);
+ }
+ prefSize.setSize(width, height);
+ editor.setSize(prefSize);
+ }
+ }
+
+ /**
+ * Sometimes we need to invoke this later because the document events seem
+ * to get fired before the text is actually incorporated into the document.
+ */
+ protected void reshapeEditorLater() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ reshapeEditor();
+ }
+ });
+ }
+
+ private static final class DefaultTextEditor extends JTextPane {
+ private static final long serialVersionUID = 1L;
+
+ public DefaultTextEditor() {
+ EmptyBorder padding = new EmptyBorder(TEXT_EDIT_PADDING,
+ TEXT_EDIT_PADDING, TEXT_EDIT_PADDING, TEXT_EDIT_PADDING);
+ setBorder(new CompoundBorder(new LineBorder(Color.black), padding));
+ }
+
+ /**
+ * Set some rendering hints - if we don't then the rendering can be
+ * inconsistent. Also, Swing doesn't work correctly with fractional
+ * metrics.
+ */
+ public void paint(final Graphics graphics) {
+ if (!(graphics instanceof Graphics2D)) {
+ throw new IllegalArgumentException("Provided graphics context is not a Graphics2D object");
+ }
+
+ final Graphics2D g2 = (Graphics2D) graphics;
+
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
+
+ super.paint(graphics);
+ }
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/event/PZoomToEventHandler.java b/src/main/java/edu/umd/cs/piccolox/event/PZoomToEventHandler.java
new file mode 100644
index 0000000..5a40881
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/event/PZoomToEventHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.event;
+
+import java.awt.event.InputEvent;
+
+import edu.umd.cs.piccolo.PCamera;
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
+import edu.umd.cs.piccolo.event.PInputEvent;
+import edu.umd.cs.piccolo.event.PInputEventFilter;
+import edu.umd.cs.piccolo.util.PBounds;
+
+/**
+ * PZoomToEventHandler is used to zoom the camera view to the node
+ * clicked on with button one.
+ *
+ * @version 1.0
+ * @author Jesse Grosjean
+ */
+public class PZoomToEventHandler extends PBasicInputEventHandler {
+ private static final int ZOOM_SPEED = 500;
+
+ /**
+ * Constructs a PZoomToEventHandler that only recognizes BUTTON1 events.
+ */
+ public PZoomToEventHandler() {
+ setEventFilter(new PInputEventFilter(InputEvent.BUTTON1_MASK));
+ }
+
+ /**
+ * Zooms the camera's view to the pressed node when button 1 is pressed.
+ *
+ * @param event event representing the mouse press
+ */
+ public void mousePressed(final PInputEvent event) {
+ zoomTo(event);
+ }
+
+ /**
+ * Zooms the camera to the picked node of the event.
+ * @param event Event from which to extract the zoom target
+ */
+ protected void zoomTo(final PInputEvent event) {
+ PBounds zoomToBounds;
+ final PNode picked = event.getPickedNode();
+
+ if (picked instanceof PCamera) {
+ final PCamera c = (PCamera) picked;
+ zoomToBounds = c.getUnionOfLayerFullBounds();
+ }
+ else {
+ zoomToBounds = picked.getGlobalFullBounds();
+ }
+
+ event.getCamera().animateViewToCenterBounds(zoomToBounds, true, ZOOM_SPEED);
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/event/package.html b/src/main/java/edu/umd/cs/piccolox/event/package.html
new file mode 100644
index 0000000..5c3cf9c
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/event/package.html
@@ -0,0 +1,34 @@
+
+
+
+This package provides additional Piccolo event handlers.
+ + diff --git a/src/main/java/edu/umd/cs/piccolox/handles/PBoundsHandle.java b/src/main/java/edu/umd/cs/piccolox/handles/PBoundsHandle.java new file mode 100644 index 0000000..2fe3269 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/handles/PBoundsHandle.java @@ -0,0 +1,449 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.handles; + +import java.awt.Cursor; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Iterator; + +import javax.swing.SwingConstants; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.event.PBasicInputEventHandler; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PDimension; +import edu.umd.cs.piccolo.util.PPickPath; +import edu.umd.cs.piccolox.util.PBoundsLocator; + +/** + * PBoundsHandle a handle for resizing the bounds of another node. If a + * bounds handle is dragged such that the other node's width or height becomes + * negative then the each drag handle's locator assciated with that other node + * is "flipped" so that they are attached to and dragging a different corner of + * the nodes bounds. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PBoundsHandle extends PHandle { + private static final long serialVersionUID = 1L; + + /** + * Event handler responsible for changing the mouse when it enters the + * handle. + */ + private transient PBasicInputEventHandler handleCursorHandler; + + /** + * Adds bounds handles to the corners and edges of the provided node. + * + * @param node node to be extended with bounds handles + */ + public static void addBoundsHandlesTo(final PNode node) { + node.addChild(new PBoundsHandle(PBoundsLocator.createEastLocator(node))); + node.addChild(new PBoundsHandle(PBoundsLocator.createWestLocator(node))); + node.addChild(new PBoundsHandle(PBoundsLocator.createNorthLocator(node))); + node.addChild(new PBoundsHandle(PBoundsLocator.createSouthLocator(node))); + node.addChild(new PBoundsHandle(PBoundsLocator.createNorthEastLocator(node))); + node.addChild(new PBoundsHandle(PBoundsLocator.createNorthWestLocator(node))); + node.addChild(new PBoundsHandle(PBoundsLocator.createSouthEastLocator(node))); + node.addChild(new PBoundsHandle(PBoundsLocator.createSouthWestLocator(node))); + } + + /** + * Adds stick handles (always visible regardless of scale since they are + * attached to the camera) to the node provided. + * + * @param node node being extended with bounds handles + * @param camera camera onto which handles will appear + */ + public static void addStickyBoundsHandlesTo(final PNode node, final PCamera camera) { + camera.addChild(new PBoundsHandle(PBoundsLocator.createEastLocator(node))); + camera.addChild(new PBoundsHandle(PBoundsLocator.createWestLocator(node))); + camera.addChild(new PBoundsHandle(PBoundsLocator.createNorthLocator(node))); + camera.addChild(new PBoundsHandle(PBoundsLocator.createSouthLocator(node))); + camera.addChild(new PBoundsHandle(PBoundsLocator.createNorthEastLocator(node))); + camera.addChild(new PBoundsHandle(PBoundsLocator.createNorthWestLocator(node))); + camera.addChild(new PBoundsHandle(PBoundsLocator.createSouthEastLocator(node))); + camera.addChild(new PBoundsHandle(PBoundsLocator.createSouthWestLocator(node))); + } + + /** + * Removes all bounds from the node provided. + * + * @param node node having its handles removed from + */ + public static void removeBoundsHandlesFrom(final PNode node) { + final ArrayList handles = new ArrayList(); + + final Iterator i = node.getChildrenIterator(); + while (i.hasNext()) { + final PNode each = (PNode) i.next(); + if (each instanceof PBoundsHandle) { + handles.add(each); + } + } + node.removeChildren(handles); + } + + /** + * Creates a bounds handle that will be attached to the provided locator. + * + * @param locator locator used to position the node + */ + public PBoundsHandle(final PBoundsLocator locator) { + super(locator); + } + + /** + * Installs the handlers to this particular bounds handle. + */ + protected void installHandleEventHandlers() { + super.installHandleEventHandlers(); + handleCursorHandler = new MouseCursorUpdateHandler(); + addInputEventListener(handleCursorHandler); + } + + /** + * Return the event handler that is responsible for setting the mouse cursor + * when it enters/exits this handle. + * + * @return current handler responsible for changing the mouse cursor + */ + public PBasicInputEventHandler getHandleCursorEventHandler() { + return handleCursorHandler; + } + + /** + * Is invoked when the a drag starts on this handle. + * + * @param aLocalPoint point in the handle's coordinate system that is + * pressed + * @param aEvent event representing the start of the drag + */ + public void startHandleDrag(final Point2D aLocalPoint, final PInputEvent aEvent) { + final PBoundsLocator l = (PBoundsLocator) getLocator(); + l.getNode().startResizeBounds(); + } + + /** + * Is invoked when the handle is being dragged. + * + * @param aLocalDimension dimension representing the magnitude of the handle + * drag + * @param aEvent event responsible for the call + */ + public void dragHandle(final PDimension aLocalDimension, final PInputEvent aEvent) { + final PBoundsLocator l = (PBoundsLocator) getLocator(); + + final PNode n = l.getNode(); + final PBounds b = n.getBounds(); + + final PNode parent = getParent(); + if (parent != n && parent instanceof PCamera) { + ((PCamera) parent).localToView(aLocalDimension); + } + + localToGlobal(aLocalDimension); + n.globalToLocal(aLocalDimension); + + final double dx = aLocalDimension.getWidth(); + final double dy = aLocalDimension.getHeight(); + + switch (l.getSide()) { + case SwingConstants.NORTH: + b.setRect(b.x, b.y + dy, b.width, b.height - dy); + break; + + case SwingConstants.SOUTH: + b.setRect(b.x, b.y, b.width, b.height + dy); + break; + + case SwingConstants.EAST: + b.setRect(b.x, b.y, b.width + dx, b.height); + break; + + case SwingConstants.WEST: + b.setRect(b.x + dx, b.y, b.width - dx, b.height); + break; + + case SwingConstants.NORTH_WEST: + b.setRect(b.x + dx, b.y + dy, b.width - dx, b.height - dy); + break; + + case SwingConstants.SOUTH_WEST: + b.setRect(b.x + dx, b.y, b.width - dx, b.height + dy); + break; + + case SwingConstants.NORTH_EAST: + b.setRect(b.x, b.y + dy, b.width + dx, b.height - dy); + break; + + case SwingConstants.SOUTH_EAST: + b.setRect(b.x, b.y, b.width + dx, b.height + dy); + break; + default: + throw new RuntimeException("Invalid side returned from PBoundsLocator"); + } + + boolean flipX = false; + boolean flipY = false; + + if (b.width < 0) { + flipX = true; + b.width = -b.width; + b.x -= b.width; + } + + if (b.height < 0) { + flipY = true; + b.height = -b.height; + b.y -= b.height; + } + + if (flipX || flipY) { + flipSiblingBoundsHandles(flipX, flipY); + } + + n.setBounds(b); + } + + /** + * Call back invoked when the drag is finished. + * + * @param aLocalPoint point on the handle where the drag was ended + * @param aEvent event responsible for the end of the drag + */ + public void endHandleDrag(final Point2D aLocalPoint, final PInputEvent aEvent) { + final PBoundsLocator l = (PBoundsLocator) getLocator(); + l.getNode().endResizeBounds(); + } + + /** + * Moves locators around so that they are still logically positioned. + * + * This is needed when a node is resized until its width or height is + * negative. + * + * @param flipX whether to allow flipping along the x direction + * @param flipY whether to allow flipping along the y direction + */ + public void flipSiblingBoundsHandles(final boolean flipX, final boolean flipY) { + final Iterator i = getParent().getChildrenIterator(); + while (i.hasNext()) { + final Object each = i.next(); + if (each instanceof PBoundsHandle) { + ((PBoundsHandle) each).flipHandleIfNeeded(flipX, flipY); + } + } + } + + /** + * Flips this bounds around if it needs to be. This is required when a node + * is resized until either its height or width is negative. + * + * @param flipX whether to allow flipping along the x direction + * @param flipY whether to allow flipping along the y direction + */ + public void flipHandleIfNeeded(final boolean flipX, final boolean flipY) { + final PBoundsLocator l = (PBoundsLocator) getLocator(); + + if (!flipX && !flipY) { + return; + } + + switch (l.getSide()) { + case SwingConstants.NORTH: + if (flipY) { + l.setSide(SwingConstants.SOUTH); + } + break; + + case SwingConstants.SOUTH: + if (flipY) { + l.setSide(SwingConstants.NORTH); + } + break; + + case SwingConstants.EAST: + if (flipX) { + l.setSide(SwingConstants.WEST); + } + break; + + case SwingConstants.WEST: + if (flipX) { + l.setSide(SwingConstants.EAST); + } + break; + + case SwingConstants.NORTH_WEST: + if (flipX && flipY) { + l.setSide(SwingConstants.SOUTH_EAST); + } + else if (flipX) { + l.setSide(SwingConstants.NORTH_EAST); + } + else if (flipY) { + l.setSide(SwingConstants.SOUTH_WEST); + } + break; + + case SwingConstants.SOUTH_WEST: + if (flipX && flipY) { + l.setSide(SwingConstants.NORTH_EAST); + } + else if (flipX) { + l.setSide(SwingConstants.SOUTH_EAST); + } + else if (flipY) { + l.setSide(SwingConstants.NORTH_WEST); + } + break; + + case SwingConstants.NORTH_EAST: + if (flipX && flipY) { + l.setSide(SwingConstants.SOUTH_WEST); + } + else if (flipX) { + l.setSide(SwingConstants.NORTH_WEST); + } + else if (flipY) { + l.setSide(SwingConstants.SOUTH_EAST); + } + break; + + case SwingConstants.SOUTH_EAST: + if (flipX && flipY) { + l.setSide(SwingConstants.NORTH_WEST); + } + else if (flipX) { + l.setSide(SwingConstants.SOUTH_WEST); + } + else if (flipY) { + l.setSide(SwingConstants.NORTH_EAST); + } + break; + + default: + throw new RuntimeException("Invalid side received from PBoundsLocator"); + } + + // reset locator to update layout + setLocator(l); + } + + /** + * Returns an appropriate handle for the given side of a node. + * + * @param side side given as SwingConstants values. + * + * @return Appropriate cursor, or null if none can be identified. + */ + public Cursor getCursorFor(final int side) { + switch (side) { + case SwingConstants.NORTH: + return new Cursor(Cursor.N_RESIZE_CURSOR); + + case SwingConstants.SOUTH: + return new Cursor(Cursor.S_RESIZE_CURSOR); + + case SwingConstants.EAST: + return new Cursor(Cursor.E_RESIZE_CURSOR); + + case SwingConstants.WEST: + return new Cursor(Cursor.W_RESIZE_CURSOR); + + case SwingConstants.NORTH_WEST: + return new Cursor(Cursor.NW_RESIZE_CURSOR); + + case SwingConstants.SOUTH_WEST: + return new Cursor(Cursor.SW_RESIZE_CURSOR); + + case SwingConstants.NORTH_EAST: + return new Cursor(Cursor.NE_RESIZE_CURSOR); + + case SwingConstants.SOUTH_EAST: + return new Cursor(Cursor.SE_RESIZE_CURSOR); + default: + return null; + } + } + + private class MouseCursorUpdateHandler extends PBasicInputEventHandler { + boolean cursorPushed; + + public MouseCursorUpdateHandler() { + cursorPushed = false; + } + + /** + * When mouse is entered, push appropriate mouse cursor on cursor stack. + * + * @param aEvent the mouse entered event + */ + public void mouseEntered(final PInputEvent aEvent) { + if (!cursorPushed) { + aEvent.pushCursor(getCursorFor(((PBoundsLocator) getLocator()).getSide())); + cursorPushed = true; + } + } + + /** + * When mouse leaves, pop cursor from stack. + * + * @param aEvent the mouse exited event + */ + public void mouseExited(final PInputEvent aEvent) { + if (cursorPushed) { + final PPickPath focus = aEvent.getInputManager().getMouseFocus(); + + if (focus == null || focus.getPickedNode() != PBoundsHandle.this) { + aEvent.popCursor(); + cursorPushed = false; + } + } + } + + /** + * If mouse is released, cursor should pop as well. + * + * @param event the mouse released event + */ + public void mouseReleased(final PInputEvent event) { + if (cursorPushed) { + event.popCursor(); + cursorPushed = false; + } + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/handles/PHandle.java b/src/main/java/edu/umd/cs/piccolox/handles/PHandle.java new file mode 100644 index 0000000..2344e0b --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/handles/PHandle.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.handles; + +import java.awt.Color; +import java.awt.Shape; +import java.awt.event.InputEvent; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.io.ObjectInputStream; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.event.PDragSequenceEventHandler; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.event.PInputEventFilter; +import edu.umd.cs.piccolo.nodes.PPath; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PDimension; +import edu.umd.cs.piccolox.util.PLocator; +import edu.umd.cs.piccolox.util.PNodeLocator; + +/** + * PHandle is used to modify some aspect of Piccolo when it is dragged. + * Each handle has a PLocator that it uses to automatically position itself. See + * PBoundsHandle for an example of a handle that resizes the bounds of another + * node. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PHandle extends PPath { + private final class HandleDragHandler extends PDragSequenceEventHandler { + protected void startDrag(final PInputEvent event) { + super.startDrag(event); + startHandleDrag(event.getPositionRelativeTo(PHandle.this), event); + } + + protected void drag(final PInputEvent event) { + super.drag(event); + final PDimension aDelta = event.getDeltaRelativeTo(PHandle.this); + if (aDelta.getWidth() != 0 || aDelta.getHeight() != 0) { + dragHandle(aDelta, event); + } + } + + protected void endDrag(final PInputEvent event) { + super.endDrag(event); + endHandleDrag(event.getPositionRelativeTo(PHandle.this), event); + } + } + + private static final long serialVersionUID = 1L; + + /** The default size for a handle. */ + public static float DEFAULT_HANDLE_SIZE = 8; + /** Default shape to use when drawing handles. */ + public static Shape DEFAULT_HANDLE_SHAPE = new Ellipse2D.Float(0f, 0f, DEFAULT_HANDLE_SIZE, DEFAULT_HANDLE_SIZE); + + /** Default color to paint handles. */ + public static Color DEFAULT_COLOR = Color.white; + + private PLocator locator; + private transient PDragSequenceEventHandler handleDragger; + + /** + * Construct a new handle that will use the given locator to locate itself + * on its parent node. + * + * @param aLocator locator to use when laying out the handle + */ + public PHandle(final PLocator aLocator) { + super(DEFAULT_HANDLE_SHAPE); + locator = aLocator; + setPaint(DEFAULT_COLOR); + installHandleEventHandlers(); + } + + /** + * Installs the handler that notify its subclasses of handle interaction. + */ + protected void installHandleEventHandlers() { + handleDragger = new HandleDragHandler(); + + addPropertyChangeListener(PNode.PROPERTY_TRANSFORM, new PropertyChangeListener() { + public void propertyChange(final PropertyChangeEvent evt) { + relocateHandle(); + } + }); + + handleDragger.setEventFilter(new PInputEventFilter(InputEvent.BUTTON1_MASK)); + handleDragger.getEventFilter().setMarksAcceptedEventsAsHandled(true); + handleDragger.getEventFilter().setAcceptsMouseEntered(false); + handleDragger.getEventFilter().setAcceptsMouseExited(false); + // no need for moved events for handle interaction, + handleDragger.getEventFilter().setAcceptsMouseMoved(false); + // so reject them so we don't consume them + addInputEventListener(handleDragger); + } + + /** + * Return the event handler that is responsible for the drag handle + * interaction. + * + * @return current handler for HandleDrag events + */ + public PDragSequenceEventHandler getHandleDraggerHandler() { + return handleDragger; + } + + /** + * Get the locator that this handle uses to position itself on its parent + * node. + * + * @return the locator associated with this handle + */ + public PLocator getLocator() { + return locator; + } + + /** + * Set the locator that this handle uses to position itself on its parent + * node. + * + * @param locator the locator to assign to this handle + */ + public void setLocator(final PLocator locator) { + this.locator = locator; + invalidatePaint(); + relocateHandle(); + } + + /** + * Override this method to get notified when the handle starts to get + * dragged. + * + * @param aLocalPoint point on the handle at which the event occurred + * @param aEvent the event responsible for starting the dragging + */ + public void startHandleDrag(final Point2D aLocalPoint, final PInputEvent aEvent) { + } + + /** + * Override this method to get notified as the handle is dragged. + * + * @param aLocalDimension size of the drag in handle coordinates + * @param aEvent event representing the drag + */ + public void dragHandle(final PDimension aLocalDimension, final PInputEvent aEvent) { + } + + /** + * Override this method to get notified when the handle stops getting + * dragged. + * + * @param aLocalPoint point in handle coordinate system of the end of the + * drag + * @param aEvent event responsible for ending the drag + */ + public void endHandleDrag(final Point2D aLocalPoint, final PInputEvent aEvent) { + } + + /** + * Set's this handle's parent. Handles respond to changes in their parent's + * bounds by invalidating themselves. + * + * @param newParent the new parent to assign to this handle + */ + public void setParent(final PNode newParent) { + super.setParent(newParent); + relocateHandle(); + } + + /** + * Forces the handles to reposition themselves using their associated + * locator. + */ + public void parentBoundsChanged() { + relocateHandle(); + } + + /** + * Force this handle to relocate itself using its locator. + */ + public void relocateHandle() { + if (locator == null) { + return; + } + + final PBounds b = getBoundsReference(); + final Point2D aPoint = locator.locatePoint(null); + + if (locator instanceof PNodeLocator) { + final PNode located = ((PNodeLocator) locator).getNode(); + final PNode parent = getParent(); + + located.localToGlobal(aPoint); + globalToLocal(aPoint); + + if (parent != located && parent instanceof PCamera) { + ((PCamera) parent).viewToLocal(aPoint); + } + } + + final double newCenterX = aPoint.getX(); + final double newCenterY = aPoint.getY(); + + if (newCenterX != b.getCenterX() || newCenterY != b.getCenterY()) { + + centerBoundsOnPoint(newCenterX, newCenterY); + } + + } + + /** + * Deserializes a PHandle from the input stream provided. Ensures tha all + * event handles are correctly installed. + * + * @param in stream from which to read the handle + * @throws IOException is thrown if the underlying input stream fails + * @throws ClassNotFoundException should never happen but can happen if the + * classpath gets messed up + */ + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + installHandleEventHandlers(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/umd/cs/piccolox/handles/PStickyHandleManager.java b/src/main/java/edu/umd/cs/piccolox/handles/PStickyHandleManager.java new file mode 100644 index 0000000..8e784c5 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/handles/PStickyHandleManager.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.handles; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PPickPath; + +/** + * This class relays adjustments to its bounds to its target. + */ +public class PStickyHandleManager extends PNode { + private static final long serialVersionUID = 1L; + private PNode target; + private PCamera camera; + + /** + * Constructs a sticky handle manager responsible for updating the position + * of its associated node on the camera provided. + * + * @param newCamera camera on which this manager is operating + * @param newTarget node to be positioned on the camera + */ + public PStickyHandleManager(final PCamera newCamera, final PNode newTarget) { + setCameraTarget(newCamera, newTarget); + PBoundsHandle.addBoundsHandlesTo(this); + } + + /** + * Changes the node and camera on which this manager is operating. + * + * @param newCamera camera on which this manager is operating + * @param newTarget node to be positioned on the camera + */ + public void setCameraTarget(final PCamera newCamera, final PNode newTarget) { + camera = newCamera; + camera.addChild(this); + target = newTarget; + } + + /** + * By changing this sticky handle's bounds, it propagates that change to its + * associated node. + * + * @param x x position of bounds + * @param y y position of bounds + * @param width width to apply to the bounds + * @param height height to apply to the bounds + * + * @return true if bounds were successfully changed + */ + public boolean setBounds(final double x, final double y, final double width, final double height) { + final PBounds b = new PBounds(x, y, width, height); + camera.localToGlobal(b); + camera.localToView(b); + target.globalToLocal(b); + target.setBounds(b); + return super.setBounds(x, y, width, height); + } + + /** + * Since this node's bounds are always dependent on its target, it is + * volatile. + * + * @return true since sticky handle manager's bounds are completely + * dependent on its children + */ + protected boolean getBoundsVolatile() { + return true; + } + + /** + * The sticky handle manager's bounds as computed by examining its target + * through its camera. + * + * @return the sticky handle manager's bounds as computed by examining its + * target through its camera + */ + public PBounds getBoundsReference() { + final PBounds targetBounds = target.getFullBounds(); + camera.viewToLocal(targetBounds); + camera.globalToLocal(targetBounds); + final PBounds bounds = super.getBoundsReference(); + bounds.setRect(targetBounds); + return super.getBoundsReference(); + } + + /** + * Dispatches this event to its target as well. + */ + public void startResizeBounds() { + super.startResizeBounds(); + target.startResizeBounds(); + } + + /** + * Dispatches this event to its target as well. + */ + public void endResizeBounds() { + super.endResizeBounds(); + target.endResizeBounds(); + } + + /** + * Since this node is invisible, it doesn't make sense to have it be + * pickable. + * + * @return false since it's invisible + * @param pickPath path in which we're trying to determine if this node is + * pickable + */ + public boolean pickAfterChildren(final PPickPath pickPath) { + return false; + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/handles/package.html b/src/main/java/edu/umd/cs/piccolox/handles/package.html new file mode 100644 index 0000000..e610ac8 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/handles/package.html @@ -0,0 +1,34 @@ + + + +This package contains handle nodes.
+ + diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/P3DRect.java b/src/main/java/edu/umd/cs/piccolox/nodes/P3DRect.java new file mode 100644 index 0000000..6aa2a11 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/nodes/P3DRect.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.nodes; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Stroke; +import java.awt.geom.GeneralPath; +import java.awt.geom.Rectangle2D; + +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PPaintContext; + +/** + * This is a simple node that draws a "3D" rectangle within the bounds of the + * node. Drawing a 3D rectangle in a zooming environment is a little tricky + * because if you just use the regular (Java2D) 3D rectangle, the 3D borders get + * scaled, and that is ugly. This version always draws the 3D border at fixed 2 + * pixel width. + * + * @author Ben Bederson + */ +public class P3DRect extends PNode { + private static final long serialVersionUID = 1L; + private Color topLeftOuterColor; + private Color topLeftInnerColor; + private Color bottomRightInnerColor; + private Color bottomRightOuterColor; + private transient GeneralPath path = null; + private transient Stroke stroke = null; + private boolean raised; + + /** + * Constructs a simple P3DRect with empty bounds and a black stroke. + */ + public P3DRect() { + raised = true; + } + + /** + * Constructs a P3DRect with the provided bounds. + * + * @param bounds bounds to assigned to the P3DRect + */ + public P3DRect(final Rectangle2D bounds) { + this(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight()); + } + + /** + * Constructs a P3DRect with the bounds provided. + * + * @param x left of bounds + * @param y top of bounds + * @param width width of bounds + * @param height height of bounds + */ + public P3DRect(final double x, final double y, final double width, final double height) { + this(); + setBounds(x, y, width, height); + } + + /** + * Sets whether this rectangle is raised off the canvas. If set to false, + * this rectangle will appear recessed into the canvas. + * + * @param raised whether the rectangle should be painted as raised or + * recessed + */ + public void setRaised(final boolean raised) { + this.raised = raised; + setPaint(getPaint()); + } + + /** + * Returns whether this P3DRect is drawn as raised. + * + * @return true if raised + */ + public boolean getRaised() { + return raised; + } + + /** + * Paints this rectangle with shaded edges. Making it appear to stand out of + * the page as normal 3D buttons do. + * + * @param paintContext context in which the paiting should occur + */ + protected void paint(final PPaintContext paintContext) { + // lazy init: + if (stroke == null) { + stroke = new BasicStroke(0); + } + if (path == null) { + path = new GeneralPath(); + } + + final Graphics2D g2 = paintContext.getGraphics(); + + final double x = getX(); + final double y = getY(); + final double width = getWidth(); + final double height = getHeight(); + final double scaleX = g2.getTransform().getScaleX(); + final double scaleY = g2.getTransform().getScaleY(); + final double dx = (float) (1.0 / scaleX); + final double dy = (float) (1.0 / scaleY); + final PBounds bounds = getBounds(); + + g2.setPaint(getPaint()); + g2.fill(bounds); + g2.setStroke(stroke); + + path.reset(); + path.moveTo((float) (x + width), (float) y); + path.lineTo((float) x, (float) y); + path.lineTo((float) x, (float) (y + height)); + g2.setPaint(topLeftOuterColor); + g2.draw(path); + + path.reset(); + path.moveTo((float) (x + width), (float) (y + dy)); + path.lineTo((float) (x + dx), (float) (y + dy)); + path.lineTo((float) (x + dx), (float) (y + height)); + g2.setPaint(topLeftInnerColor); + g2.draw(path); + + path.reset(); + path.moveTo((float) (x + width), (float) y); + path.lineTo((float) (x + width), (float) (y + height)); + path.lineTo((float) x, (float) (y + height)); + g2.setPaint(bottomRightOuterColor); + g2.draw(path); + + path.reset(); + path.moveTo((float) (x + width - dx), (float) (y + dy)); + path.lineTo((float) (x + width - dx), (float) (y + height - dy)); + path.lineTo((float) x, (float) (y + height - dy)); + g2.setPaint(bottomRightInnerColor); + g2.draw(path); + } + + /** + * Changes the paint that will be used to draw this rectangle. This paint is + * used to shade the edges of the rectangle. + * + * @param newPaint the color to use for painting this rectangle + */ + public void setPaint(final Paint newPaint) { + super.setPaint(newPaint); + + if (newPaint instanceof Color) { + final Color color = (Color) newPaint; + if (raised) { + setRaisedEdges(color); + } + else { + setRecessedEdges(color); + } + } + else { + setNoEdges(); + } + } + + private void setRaisedEdges(final Color color) { + topLeftOuterColor = color.brighter(); + topLeftInnerColor = topLeftOuterColor.brighter(); + bottomRightInnerColor = color.darker(); + bottomRightOuterColor = bottomRightInnerColor.darker(); + } + + private void setNoEdges() { + topLeftOuterColor = null; + topLeftInnerColor = null; + bottomRightInnerColor = null; + bottomRightOuterColor = null; + } + + private void setRecessedEdges(final Color color) { + topLeftOuterColor = color.darker(); + topLeftInnerColor = topLeftOuterColor.darker(); + bottomRightInnerColor = color.brighter(); + bottomRightOuterColor = bottomRightInnerColor.brighter(); + } + + /** + * TODO can we remove this? + * + * @deprecated since it has been moved to P3DRectExample. + * + * @param args Command line arguments + */ + public static void main(final String[] args) { + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/PCacheCamera.java b/src/main/java/edu/umd/cs/piccolox/nodes/PCacheCamera.java new file mode 100644 index 0000000..1505445 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/nodes/PCacheCamera.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.nodes; + +import java.awt.Color; +import java.awt.GraphicsEnvironment; +import java.awt.Paint; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PRoot; +import edu.umd.cs.piccolo.activities.PTransformActivity; +import edu.umd.cs.piccolo.util.PAffineTransform; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PDimension; +import edu.umd.cs.piccolo.util.PPaintContext; +import edu.umd.cs.piccolo.util.PUtil; + +/** + * An extension to PCamera that provides a fast image based + * animationToCenterBounds method. + * + * @author Lance Good + */ +public class PCacheCamera extends PCamera { + + private static final long serialVersionUID = 1L; + private transient BufferedImage paintBuffer; + private boolean imageAnimate; + private PBounds imageAnimateBounds; + + /** + * Get the buffer used to provide fast image based animation. + * + * @return buffered image used to provide fast image based animation + */ + protected BufferedImage getPaintBuffer() { + final PBounds fRef = getFullBoundsReference(); + if (paintBuffer == null || isBufferSmallerThanBounds(fRef)) { + paintBuffer = buildPaintBuffer(fRef); + } + return paintBuffer; + } + + private boolean isBufferSmallerThanBounds(final PBounds bounds) { + return paintBuffer.getWidth() < bounds.getWidth() || paintBuffer.getHeight() < bounds.getHeight(); + } + + private BufferedImage buildPaintBuffer(final PBounds fRef) { + final int newBufferWidth = (int) Math.ceil(fRef.getWidth()); + final int newBufferHeight = (int) Math.ceil(fRef.getHeight()); + + if (GraphicsEnvironment.isHeadless()) { + return new BufferedImage(newBufferWidth, newBufferHeight, BufferedImage.TYPE_4BYTE_ABGR); + } + else { + return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration() + .createCompatibleImage(newBufferWidth, newBufferHeight); + } + } + + /** + * Caches the information necessary to animate from the current view bounds + * to the specified centerBounds. + */ + private AffineTransform cacheViewBounds(final Rectangle2D centerBounds, final boolean scaleToFit) { + final PBounds viewBounds = getViewBounds(); + + // Initialize the image to the union of the current and destination + // bounds + final PBounds imageBounds = new PBounds(viewBounds); + imageBounds.add(centerBounds); + + animateViewToCenterBounds(imageBounds, scaleToFit, 0); + + imageAnimateBounds = getViewBounds(); + + // Now create the actual cache image that we will use to animate fast + + final BufferedImage buffer = getPaintBuffer(); + Paint fPaint = Color.white; + if (getPaint() != null) { + fPaint = getPaint(); + } + toImage(buffer, fPaint); + + // Do this after the painting above! + imageAnimate = true; + + // Return the bounds to the previous viewbounds + animateViewToCenterBounds(viewBounds, scaleToFit, 0); + + // The code below is just copied from animateViewToCenterBounds to + // create the correct transform to center the specified bounds + + final PDimension delta = viewBounds.deltaRequiredToCenter(centerBounds); + final PAffineTransform newTransform = getViewTransform(); + newTransform.translate(delta.width, delta.height); + + if (scaleToFit) { + final double s = Math.min(viewBounds.getWidth() / centerBounds.getWidth(), viewBounds.getHeight() + / centerBounds.getHeight()); + newTransform.scaleAboutPoint(s, centerBounds.getCenterX(), centerBounds.getCenterY()); + } + + return newTransform; + } + + /** + * Turns off the fast image animation and does any other applicable cleanup. + */ + private void clearViewCache() { + imageAnimate = false; + imageAnimateBounds = null; + } + + /** + * Mimics the standard animateViewToCenterBounds but uses a cached image for + * performance rather than re-rendering the scene at each step. + * + * @param centerBounds bounds to which the view should be centered + * @param shouldScaleToFit whether the camera should scale to fit the bounds + * so the cover as large a portion of the canvas without changing + * the aspect ratio + * @param duration milliseconds the animation should last + * @return the scheduled activity, null if duration was 0 + */ + public PTransformActivity animateStaticViewToCenterBoundsFast(final Rectangle2D centerBounds, + final boolean shouldScaleToFit, final long duration) { + if (duration == 0) { + return animateViewToCenterBounds(centerBounds, shouldScaleToFit, duration); + } + + final AffineTransform newViewTransform = cacheViewBounds(centerBounds, shouldScaleToFit); + + return animateStaticViewToTransformFast(newViewTransform, duration); + } + + /** + * This copies the behavior of the standard animateViewToTransform but + * clears the cache when it is done. + * + * @param dest the resulting transform that the view should be + * applying when the animation is complete + * @param duration length in milliseconds that the animation should last + * @return the scheduled PTransformActivity, null if duration was 0 + */ + protected PTransformActivity animateStaticViewToTransformFast(final AffineTransform dest, final long duration) { + if (duration == 0) { + setViewTransform(dest); + return null; + } + + final PTransformActivity.Target t = new PTransformActivity.Target() { + public void setTransform(final AffineTransform aTransform) { + PCacheCamera.this.setViewTransform(aTransform); + } + + public void getSourceMatrix(final double[] aSource) { + getViewTransformReference().getMatrix(aSource); + } + }; + + final PTransformActivity ta = new PTransformActivity(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, t, dest) { + protected void activityFinished() { + clearViewCache(); + repaint(); + super.activityFinished(); + } + }; + + final PRoot r = getRoot(); + if (r != null) { + r.getActivityScheduler().addActivity(ta); + } + + return ta; + } + + /** + * Overrides the camera's full paint method to do the fast rendering when + * possible. + * + * @param paintContext Paint Contex in which the painting is done + */ + public void fullPaint(final PPaintContext paintContext) { + if (imageAnimate) { + final PBounds fRef = getFullBoundsReference(); + final PBounds viewBounds = getViewBounds(); + final double scale = getFullBoundsReference().getWidth() / imageAnimateBounds.getWidth(); + final double xOffset = (viewBounds.getX() - imageAnimateBounds.getX()) * scale; + final double yOffset = (viewBounds.getY() - imageAnimateBounds.getY()) * scale; + final double scaleW = viewBounds.getWidth() * scale; + final double scaleH = viewBounds.getHeight() * scale; + paintContext.getGraphics().drawImage(paintBuffer, 0, 0, (int) Math.ceil(fRef.getWidth()), + (int) Math.ceil(fRef.getHeight()), (int) Math.floor(xOffset), (int) Math.floor(yOffset), + (int) Math.ceil(xOffset + scaleW), (int) Math.ceil(yOffset + scaleH), null); + } + else { + super.fullPaint(paintContext); + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/PClip.java b/src/main/java/edu/umd/cs/piccolox/nodes/PClip.java new file mode 100644 index 0000000..66fdafa --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/nodes/PClip.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.nodes; + +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.geom.Rectangle2D; + +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.nodes.PPath; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PPaintContext; +import edu.umd.cs.piccolo.util.PPickPath; + +/** + * PClip is a simple node that applies a clip before rendering or picking + * its children. PClip is a subclass of PPath, the clip applies is the + * GeneralPath wrapped by its super class. See piccolo2d/examples ClipExample. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PClip extends PPath { + private static final long serialVersionUID = 1L; + + /** + * Computes the full bounds and stores them in dstBounds, if dstBounds is + * null, create a new Bounds and returns it. + * + * @param dstBounds output parameter where computed bounds will be stored + * @return the computed full bounds + */ + public PBounds computeFullBounds(final PBounds dstBounds) { + final PBounds result; + if (dstBounds == null) { + result = new PBounds(); + } + else { + result = dstBounds; + result.reset(); + } + + result.add(getBoundsReference()); + localToParent(result); + return result; + } + + /** + * Callback that receives notification of repaint requests from nodes in + * this node's tree. + * + * @param localBounds region in local coordinations the needs repainting + * @param childOrThis the node that emitted the repaint notification + */ + public void repaintFrom(final PBounds localBounds, final PNode childOrThis) { + if (childOrThis != this) { + Rectangle2D.intersect(getBoundsReference(), localBounds, localBounds); + super.repaintFrom(localBounds, childOrThis); + } + else { + super.repaintFrom(localBounds, childOrThis); + } + } + + /** + * Paint's this node as a solid rectangle if paint is provided, clipping + * appropriately. + * + * @param paintContext context into which this node will be painted + */ + protected void paint(final PPaintContext paintContext) { + final Paint p = getPaint(); + if (p != null) { + final Graphics2D g2 = paintContext.getGraphics(); + g2.setPaint(p); + g2.fill(getPathReference()); + } + paintContext.pushClip(getPathReference()); + } + + /** + * Paints a border around this node if it has a stroke and stroke paint + * provided. + * + * @param paintContext context into which the border will be drawn + */ + protected void paintAfterChildren(final PPaintContext paintContext) { + paintContext.popClip(getPathReference()); + if (getStroke() != null && getStrokePaint() != null) { + final Graphics2D g2 = paintContext.getGraphics(); + g2.setPaint(getStrokePaint()); + g2.setStroke(getStroke()); + g2.draw(getPathReference()); + } + } + + /** + * Try to pick this node and all of its descendants if they are visible in + * the clipping region. + * + * @param pickPath the pick path to add the node to if its picked + * @return true if this node or one of its descendants was picked. + */ + public boolean fullPick(final PPickPath pickPath) { + if (getPickable() && fullIntersects(pickPath.getPickBounds())) { + pickPath.pushNode(this); + pickPath.pushTransform(getTransformReference(false)); + + if (pick(pickPath)) { + return true; + } + + if (getChildrenPickable() && getPathReference().intersects(pickPath.getPickBounds())) { + final int count = getChildrenCount(); + for (int i = count - 1; i >= 0; i--) { + final PNode each = getChild(i); + if (each.fullPick(pickPath)) { + return true; + } + } + } + + if (pickAfterChildren(pickPath)) { + return true; + } + + pickPath.popTransform(getTransformReference(false)); + pickPath.popNode(this); + } + + return false; + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/PComposite.java b/src/main/java/edu/umd/cs/piccolox/nodes/PComposite.java new file mode 100644 index 0000000..e943c15 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/nodes/PComposite.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.nodes; + +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.util.PPickPath; + +/** + * PComposite is a simple node that makes a group of nodes appear to be a + * single node when picking and interacting. There is also partial (commented + * out) support for resizing the child node to fit when this nodes bounds are + * set. + * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PComposite extends PNode { + + /* + * public boolean setBounds(double x, double y, double width, double height) + * { PBounds childBounds = getUnionOfChildrenBounds(null); + * + * double dx = x - childBounds.x; double dy = y - childBounds.y; double sx = + * width / childBounds.width; double sy = height / childBounds.height; + * double scale = sx > sy ? sx : sy; + * + * Iterator i = getChildrenIterator(); while (i.hasNext()) { PNode each = + * (PNode) i.next(); each.offset(dx, dy); each.scaleAboutPoint(scale, + * each.getBoundsReference().x, each.getBoundsReference().y); } + * + * return super.setBounds(x, y, width, height); } + * + * protected void layoutChildren() { + * getBoundsReference().setRect(getUnionOfChildrenBounds(null)); } + */ + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Return true if this node or any pickable descendants are picked. If a + * pick occurs the pickPath is modified so that this node is always returned + * as the picked node, event if it was a descendant node that initially + * reported the pick. + * + * @param pickPath the pick path to add the nodes to if they are picked + * @return true if this node or one of its descendants was picked + */ + public boolean fullPick(final PPickPath pickPath) { + if (super.fullPick(pickPath)) { + PNode picked = pickPath.getPickedNode(); + + // this code won't work with internal cameras, because it doesn't + // pop the cameras view transform. + while (picked != this) { + pickPath.popTransform(picked.getTransformReference(false)); + pickPath.popNode(picked); + picked = pickPath.getPickedNode(); + } + + return true; + } + return false; + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/PLens.java b/src/main/java/edu/umd/cs/piccolox/nodes/PLens.java new file mode 100644 index 0000000..31e13b3 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/nodes/PLens.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.nodes; + +import java.awt.Color; +import java.awt.Paint; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PLayer; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.event.PDragEventHandler; +import edu.umd.cs.piccolo.nodes.PPath; + +/** + * PLens is a simple default lens implementation for Piccolo2D. See + * piccolo2d/examples LensExample for one possible use of this lens. Lens's are + * often application specific, it may be easiest to study this code, and then + * implement your own custom lens using the general principles illustrated here. + *+ * The basic design here is to add a PCamera as the child of a PNode (the lens + * node). The camera is the viewing part of the lens, and the node is the title + * bar that can be used to move the lens around. Users of this lens will + * probably want to set up some lens specific event handler and attach it to the + * camera. + *
+ *+ * A lens also needs a layer that it will look at (it should not be the same as + * the layer that it's added to because then it will draw itself in a recursive + * loop. Last of all the PLens will need to be added to the PCanvas layer (so + * that it can be seen by the main camera). + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PLens extends PNode { + + private static final long serialVersionUID = 1L; + private final PPath dragBar; + private final PCamera camera; + private final transient PDragEventHandler lensDragger; + + /** The height of the drag bar. */ + public static double LENS_DRAGBAR_HEIGHT = 20; + + /** Default paint to use for the drag bar. */ + public static Paint DEFAULT_DRAGBAR_PAINT = Color.DARK_GRAY; + + /** Default paint to use when drawing the background of the lens. */ + public static Paint DEFAULT_LENS_PAINT = Color.LIGHT_GRAY; + + /** + * Constructs the default PLens. + */ + public PLens() { + // Drag bar gets resized to fit the available space, so any rectangle + // will do here + dragBar = PPath.createRectangle(0, 0, 1, 1); + dragBar.setPaint(DEFAULT_DRAGBAR_PAINT); + // This forces drag events to percolate up to PLens object + dragBar.setPickable(false); + addChild(dragBar); + + camera = new PCamera(); + camera.setPaint(DEFAULT_LENS_PAINT); + addChild(camera); + + // create an event handler to drag the lens around. Note that this event + // handler consumes events in case another conflicting event handler has + // been installed higher up in the heirarchy. + lensDragger = new PDragEventHandler(); + lensDragger.getEventFilter().setMarksAcceptedEventsAsHandled(true); + addInputEventListener(lensDragger); + + // When this PLens is dragged around adjust the cameras view transform. + addPropertyChangeListener(PNode.PROPERTY_TRANSFORM, new PropertyChangeListener() { + public void propertyChange(final PropertyChangeEvent evt) { + camera.setViewTransform(getInverseTransform()); + } + }); + } + + /** + * Creates the default PLens and attaches the given layer to it. + * + * @param layer layer to attach to this PLens + */ + public PLens(final PLayer layer) { + this(); + addLayer(0, layer); + } + + /** + * Returns the camera on which this lens is appearing. + * + * @return camera on which lens is appearing + */ + public PCamera getCamera() { + return camera; + } + + /** + * Returns the drag bar for this lens. + * + * @return this lens' drag bar + */ + public PPath getDragBar() { + return dragBar; + } + + /** + * Returns the event handler that this lens uses for its drag bar. + * + * @return drag bar's drag event handler + */ + public PDragEventHandler getLensDraggerHandler() { + return lensDragger; + } + + /** + * Adds the layer to the camera. + * + * @param index index at which to add the layer to the camera + * @param layer layer to add to the camera + */ + public void addLayer(final int index, final PLayer layer) { + camera.addLayer(index, layer); + } + + /** + * Removes the provided layer from the camera. + * + * @param layer layer to be removed + */ + public void removeLayer(final PLayer layer) { + camera.removeLayer(layer); + } + + /** + * When the lens is resized this method gives us a chance to layout the + * lenses camera child appropriately. + */ + protected void layoutChildren() { + dragBar.setPathToRectangle((float) getX(), (float) getY(), (float) getWidth(), (float) LENS_DRAGBAR_HEIGHT); + camera.setBounds(getX(), getY() + LENS_DRAGBAR_HEIGHT, getWidth(), getHeight() - LENS_DRAGBAR_HEIGHT); + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/PLine.java b/src/main/java/edu/umd/cs/piccolox/nodes/PLine.java new file mode 100644 index 0000000..4657322 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/nodes/PLine.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.nodes; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Stroke; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.nodes.PPath; +import edu.umd.cs.piccolo.util.PAffineTransform; +import edu.umd.cs.piccolo.util.PPaintContext; +import edu.umd.cs.piccolo.util.PUtil; +import edu.umd.cs.piccolox.util.LineShape; + +/** + * PLine a class for drawing multisegment lines. + * + * @author Hallvard Traetteberg. + */ +public class PLine extends PNode { + + private static final long serialVersionUID = 1L; + private static final PAffineTransform TEMP_TRANSFORM = new PAffineTransform(); + private static final BasicStroke DEFAULT_STROKE = new BasicStroke(1.0f); + private static final Color DEFAULT_STROKE_PAINT = Color.black; + + private final transient LineShape lineShape; + private transient Stroke stroke; + private Paint strokePaint; + + /** + * Constructs a new PLine with an empty LineShape. + */ + public PLine() { + this(null); + } + + /** + * Constructs a PLine object for displaying the provided line. + * + * @param lineShape will be displayed by this PLine + */ + public PLine(final LineShape lineShape) { + strokePaint = DEFAULT_STROKE_PAINT; + stroke = DEFAULT_STROKE; + + if (lineShape == null) { + this.lineShape = new LineShape(null); + } + else { + this.lineShape = lineShape; + } + } + + /** + * Constructs a PLine for the given lineShape and the given stroke. + * + * @param line line to be wrapped by this PLine + * @param aStroke stroke to use when drawling the line + */ + public PLine(final LineShape line, final Stroke aStroke) { + this(line); + stroke = aStroke; + } + + /** + * Returns the paint to be used while drawing the line. + * + * @return paint used when drawing the line + */ + public Paint getStrokePaint() { + return strokePaint; + } + + /** + * Changes the paint to be used while drawing the line. + * + * @param newStrokePaint paint to use when drawing the line + */ + public void setStrokePaint(final Paint newStrokePaint) { + final Paint oldPaint = strokePaint; + strokePaint = newStrokePaint; + invalidatePaint(); + firePropertyChange(PPath.PROPERTY_CODE_STROKE_PAINT, PPath.PROPERTY_STROKE_PAINT, oldPaint, strokePaint); + } + + /** + * Returns the stroke that will be used when drawing the line. + * + * @return stroke used to draw the line + */ + public Stroke getStroke() { + return stroke; + } + + /** + * Sets stroke to use when drawing the line. + * + * @param newStroke stroke to use when drawing the line + */ + public void setStroke(final Stroke newStroke) { + final Stroke oldStroke = stroke; + stroke = newStroke; + updateBoundsFromLine(); + invalidatePaint(); + firePropertyChange(PPath.PROPERTY_CODE_STROKE, PPath.PROPERTY_STROKE, oldStroke, stroke); + } + + /** {@inheritDoc} */ + public boolean setBounds(final double x, final double y, final double width, final double height) { + if (lineShape == null || !super.setBounds(x, y, width, height)) { + return false; + } + + final Rectangle2D lineBounds = lineShape.getBounds2D(); + final Rectangle2D lineStrokeBounds = getLineBoundsWithStroke(); + final double strokeOutset = Math.max(lineStrokeBounds.getWidth() - lineBounds.getWidth(), lineStrokeBounds + .getHeight() + - lineBounds.getHeight()); + + double adjustedX = x + strokeOutset / 2; + double adjustedY = y + strokeOutset / 2; + double adjustedWidth = width - strokeOutset; + double adjustedHeight = height - strokeOutset; + + TEMP_TRANSFORM.setToIdentity(); + TEMP_TRANSFORM.translate(adjustedX, adjustedY); + TEMP_TRANSFORM.scale(adjustedWidth / lineBounds.getWidth(), adjustedHeight / lineBounds.getHeight()); + TEMP_TRANSFORM.translate(-lineBounds.getX(), -lineBounds.getY()); + lineShape.transformPoints(TEMP_TRANSFORM); + + return true; + } + + /** {@inheritDoc} */ + public boolean intersects(final Rectangle2D aBounds) { + if (super.intersects(aBounds)) { + if (lineShape.intersects(aBounds)) { + return true; + } + else if (stroke != null && strokePaint != null) { + return stroke.createStrokedShape(lineShape).intersects(aBounds); + } + } + return false; + } + + /** + * Calculates the bounds of the line taking stroke width into account. + * + * @return rectangle representing the bounds of the line taking stroke width + * into account + */ + public Rectangle2D getLineBoundsWithStroke() { + if (stroke != null) { + return stroke.createStrokedShape(lineShape).getBounds2D(); + } + else { + return lineShape.getBounds2D(); + } + } + + /** + * Recalculates the bounds when a change to the underlying line occurs. + */ + public void updateBoundsFromLine() { + if (lineShape.getPointCount() == 0) { + resetBounds(); + } + else { + final Rectangle2D b = getLineBoundsWithStroke(); + super.setBounds(b.getX(), b.getY(), b.getWidth(), b.getHeight()); + } + } + + /** + * Paints the PLine in the provided context if it has both a stroke and a + * stroke paint assigned. + * + * @param paintContext the context into which the line should be drawn + */ + protected void paint(final PPaintContext paintContext) { + final Graphics2D g2 = paintContext.getGraphics(); + + if (stroke != null && strokePaint != null) { + g2.setPaint(strokePaint); + g2.setStroke(stroke); + g2.draw(lineShape); + } + } + + /** + * Returns a reference to the underlying line shape. Be careful! + * + * @return direct reference to the underlying line shape + */ + public LineShape getLineReference() { + return lineShape; + } + + /** + * Returns the number of points in the line. + * + * @return number of points in the line + */ + public int getPointCount() { + return lineShape.getPointCount(); + } + + /** + * Returns the point at the provided index. If dst is not null, it will + * populate it with the point's coordinates rather than create a new point. + * + * @param pointIndex index of desired point in line + * @param dst point to populate, may be null + * @return the desired point, or dst populate with its coordinates + */ + public Point2D getPoint(final int pointIndex, final Point2D dst) { + final Point2D result; + if (dst == null) { + result = new Point2D.Double(); + } + else { + result = dst; + } + return lineShape.getPoint(pointIndex, result); + } + + /** + * Fires appropriate change events, updates line bounds and flags the PLine + * as requiring a repaint. + */ + protected void lineChanged() { + firePropertyChange(PPath.PROPERTY_CODE_PATH, PPath.PROPERTY_PATH, null, lineShape); + updateBoundsFromLine(); + invalidatePaint(); + } + + /** + * Changes the point at the provided index. + * + * @param pointIndex index of point to change + * @param x x component to assign to the point + * @param y y component to assign to the point + */ + public void setPoint(final int pointIndex, final double x, final double y) { + lineShape.setPoint(pointIndex, x, y); + lineChanged(); + } + + /** + * Inserts a point at the provided index. + * + * @param pointIndex index at which to add the point + * @param x x component of new point + * @param y y component of new point + */ + public void addPoint(final int pointIndex, final double x, final double y) { + lineShape.addPoint(pointIndex, x, y); + lineChanged(); + } + + /** + * Removes points from the line. + * + * @param startIndex index from which to remove the points + * @param numberOfPoints number of points to remove + */ + public void removePoints(final int startIndex, final int numberOfPoints) { + lineShape.removePoints(startIndex, numberOfPoints); + lineChanged(); + } + + /** + * Removes all points from the underlying line. + */ + public void removeAllPoints() { + lineShape.removePoints(0, lineShape.getPointCount()); + lineChanged(); + } + + private void writeObject(final ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + PUtil.writeStroke(stroke, out); + } + + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + stroke = PUtil.readStroke(in); + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/PNodeCache.java b/src/main/java/edu/umd/cs/piccolox/nodes/PNodeCache.java new file mode 100644 index 0000000..455c161 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/nodes/PNodeCache.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.nodes; + +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.geom.Dimension2D; + +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PDimension; +import edu.umd.cs.piccolo.util.PPaintContext; +import edu.umd.cs.piccolo.util.PPickPath; + +/** + * PNodeCache caches a visual representation of it's children into an + * image and uses this cached image for painting instead of painting it's + * children directly. This is intended to be used in two ways. + *+ * First it can be used as a simple optimization technique. If a node has many + * descendents it may be faster to paint the cached image representation instead + * of painting each node. + *
+ *+ * Second PNodeCache provides a place where "image" effects such as blurring and + * drop shadows can be added to the Piccolo scene graph. This can be done by + * overriding the method createImageCache and returing an image with the desired + * effect applied. + *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PNodeCache extends PNode { + private static final long serialVersionUID = 1L; + private transient Image imageCache; + private boolean validatingCache; + + /** + * Override this method to customize the image cache creation process. For + * example if you want to create a shadow effect you would do that here. + * Fill in the cacheOffsetRef if needed to make your image cache line up + * with the nodes children. + * + * @param cacheOffsetRef output parameter that can be changed to make the + * cached offset line up with the node's children + * @return an image representing this node + */ + public Image createImageCache(final Dimension2D cacheOffsetRef) { + return toImage(); + } + + /** + * Returns an image that is a cached representation of its children. + * + * @return image representation of its children + */ + public Image getImageCache() { + if (imageCache == null) { + final PDimension cacheOffsetRef = new PDimension(); + validatingCache = true; + resetBounds(); + imageCache = createImageCache(cacheOffsetRef); + final PBounds b = getFullBoundsReference(); + setBounds(b.getX() + cacheOffsetRef.getWidth(), b.getY() + cacheOffsetRef.getHeight(), imageCache + .getWidth(null), imageCache.getHeight(null)); + validatingCache = false; + } + return imageCache; + } + + /** + * Clears the cache, forcing it to be recalculated on the next call to + * getImageCache. + */ + public void invalidateCache() { + imageCache = null; + } + + /** + * Intercepts the normal invalidatePaint mechanism so that the node will not + * be repainted unless it's cache has been invalidated. + */ + public void invalidatePaint() { + if (!validatingCache) { + super.invalidatePaint(); + } + } + + /** + * Handles a repaint event issued from a node in this node's tree. + * + * @param localBounds local bounds of this node that need repainting + * @param childOrThis the node that emitted the repaint notification + */ + public void repaintFrom(final PBounds localBounds, final PNode childOrThis) { + if (!validatingCache) { + super.repaintFrom(localBounds, childOrThis); + invalidateCache(); + } + } + + /** + * Repaints this node, using the cached result if possible. + * + * @param paintContext context in which painting should occur + */ + public void fullPaint(final PPaintContext paintContext) { + if (validatingCache) { + super.fullPaint(paintContext); + } + else { + final Graphics2D g2 = paintContext.getGraphics(); + g2.drawImage(getImageCache(), (int) getX(), (int) getY(), null); + } + } + + /** + * By always returning false, makes the PNodeCache instance NOT pickable. + * + * @param pickPath path which this node is being tested for inclusion + * @return always returns false + */ + protected boolean pickAfterChildren(final PPickPath pickPath) { + return false; + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/PShadow.java b/src/main/java/edu/umd/cs/piccolox/nodes/PShadow.java new file mode 100644 index 0000000..acf6615 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/nodes/PShadow.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.nodes; + +import java.awt.Image; +import java.awt.Paint; + +import edu.umd.cs.piccolo.nodes.PImage; + +import edu.umd.cs.piccolox.util.ShadowUtils; + +/** + * Shadow node. + * + * @since 1.3 + */ +public final class PShadow extends PImage { + + /** Default serial version UID. */ + private static final long serialVersionUID = 1L; + + + /** + * Create a new shadow node containing a shadow of the specified source image using the + * specified shadow paint and gaussian blur radius. The dimensions of this node will be + *src.getWidth() + 4 * blurRadius
x src.getHeight() + 4 * blurRadius
+ * to account for blurring beyond the bounds of the source image. Thus the source image
+ * will appear to be be offset by (2 * blurRadius
, 2 * blurRadius
)
+ * in this node.
+ *
+ * @param src source image, must not be null
+ * @param shadowPaint shadow paint
+ * @param blurRadius gaussian blur radius, must be > 0
+ */
+ public PShadow(final Image src, final Paint shadowPaint, final int blurRadius) {
+ super(ShadowUtils.createShadow(src, shadowPaint, blurRadius));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/PStyledText.java b/src/main/java/edu/umd/cs/piccolox/nodes/PStyledText.java
new file mode 100644
index 0000000..2899f57
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/nodes/PStyledText.java
@@ -0,0 +1,798 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.nodes;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.Insets;
+import java.awt.font.FontRenderContext;
+import java.awt.font.LineBreakMeasurer;
+import java.awt.font.TextAttribute;
+import java.awt.font.TextLayout;
+import java.awt.geom.Line2D;
+import java.awt.geom.Rectangle2D;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import javax.swing.text.AttributeSet;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.DefaultStyledDocument;
+import javax.swing.text.Document;
+import javax.swing.text.Element;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyleContext;
+
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.util.PPaintContext;
+
+/**
+ * @author Lance Good
+ */
+public class PStyledText extends PNode {
+
+ private static final long serialVersionUID = 1L;
+
+ /** Font rendering context used for all PStyledText instances. */
+ protected static FontRenderContext SWING_FRC = new FontRenderContext(null, true, false);
+
+ /** Used while painting underlines. */
+ protected static Line2D paintLine = new Line2D.Double();
+
+ /**
+ * Underlying document used to handle the complexities involved with
+ * arbitrary text and formatting.
+ */
+ protected Document document;
+
+ /** String contents of the document. */
+ protected transient ArrayList stringContents;
+
+ /** Tracks the information about line metrics within the document. */
+ protected transient LineInfo[] lines;
+
+ /** Whether this node is currently being edited. */
+ protected boolean editing;
+
+ /** Insets represent how far away from the bounding box text will be drawn. */
+ protected Insets insets = new Insets(0, 0, 0, 0);
+
+ /** Whether width will be forced to match containing text's height. */
+ protected boolean constrainHeightToTextHeight = true;
+
+ /** Whether width will be forced to match containing text's width. */
+ protected boolean constrainWidthToTextWidth = true;
+
+ /**
+ * Constructs an empty PStyledText element.
+ */
+ public PStyledText() {
+ }
+
+ /**
+ * Controls whether this node changes its width to fit the width of its
+ * text. If flag is true it does; if flag is false it doesn't
+ *
+ * @param constrainWidthToTextWidth whether node's width should be
+ * constrained to the width of its text
+ */
+ public void setConstrainWidthToTextWidth(final boolean constrainWidthToTextWidth) {
+ this.constrainWidthToTextWidth = constrainWidthToTextWidth;
+ recomputeLayout();
+ }
+
+ /**
+ * Controls whether this node changes its height to fit the height of its
+ * text. If flag is true it does; if flag is false it doesn't
+ *
+ * @param constrainHeightToTextHeight whether node's height should be
+ * constrained to the height of its text
+ */
+ public void setConstrainHeightToTextHeight(final boolean constrainHeightToTextHeight) {
+ this.constrainHeightToTextHeight = constrainHeightToTextHeight;
+ recomputeLayout();
+ }
+
+ /**
+ * Controls whether this node changes its width to fit the width of its
+ * text. If flag is true it does; if flag is false it doesn't
+ *
+ * @return true if node is constrained to the width of its text
+ */
+ public boolean getConstrainWidthToTextWidth() {
+ return constrainWidthToTextWidth;
+ }
+
+ /**
+ * Controls whether this node changes its height to fit the height of its
+ * text. If flag is true it does; if flag is false it doesn't
+ *
+ * @return true if node is constrained to the height of its text
+ */
+ public boolean getConstrainHeightToTextHeight() {
+ return constrainHeightToTextHeight;
+ }
+
+ /**
+ * Get the document for this PStyledText. Document is used as the node's
+ * model.
+ *
+ * @return internal document used as a model of this PStyledText
+ */
+ public Document getDocument() {
+ return document;
+ }
+
+ /**
+ * Set the document on this PStyledText. Document is used as the node's
+ * model.
+ *
+ * @param document to be used as the model for this PStyledText
+ */
+ public void setDocument(final Document document) {
+ // Save the document
+ this.document = document;
+
+ syncWithDocument();
+ }
+
+ /**
+ * Enforce that the current display matches the styling of the underlying
+ * document as closely as possible.
+ */
+ public void syncWithDocument() {
+ // First get the actual text and stick it in an Attributed String
+ stringContents = new ArrayList();
+
+ String documentString;
+ try {
+ documentString = document.getText(0, document.getLength());
+ }
+ catch (final BadLocationException e) {
+ // Since this the location we're providing comes from directly
+ // querying the document, this is impossible in a single threaded
+ // model
+ return;
+ }
+
+ // The paragraph start and end indices
+ final ArrayList pEnds = extractParagraphRanges(documentString);
+
+ // The default style context - which will be reused
+ final StyleContext styleContext = StyleContext.getDefaultStyleContext();
+
+ int pos;
+ RunInfo paragraphRange = null;
+
+ AttributedString attributedString;
+
+ final Iterator contentIterator = stringContents.iterator();
+ final Iterator paragraphIterator = pEnds.iterator();
+ while (contentIterator.hasNext() && paragraphIterator.hasNext()) {
+ paragraphRange = (RunInfo) paragraphIterator.next();
+ attributedString = (AttributedString) contentIterator.next();
+ pos = paragraphRange.startIndex;
+
+ // The current element will be used as a temp variable while
+ // searching for the leaf element at the current position
+ Element curElement = null;
+
+ // Small assumption here that there is one root element - can fix
+ // for more general support later
+ final Element rootElement = document.getDefaultRootElement();
+
+ // If the string is length 0 then we just need to add the attributes
+ // once
+ if (paragraphRange.isEmpty()) {
+ curElement = drillDownFromRoot(pos, rootElement);
+
+ // These are the mandatory attributes
+ final AttributeSet attributes = curElement.getAttributes();
+ final Color foreground = styleContext.getForeground(attributes);
+
+ attributedString.addAttribute(TextAttribute.FOREGROUND, foreground, Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), Math.min(paragraphRange.length(), curElement.getEndOffset()
+ - paragraphRange.startIndex));
+
+ final Font font = extractFont(styleContext, pos, rootElement, attributes);
+ applyFontAttribute(paragraphRange, attributedString, curElement, font);
+ applyBackgroundAttribute(styleContext, paragraphRange, attributedString, curElement, attributes);
+ applyUnderlineAttribute(paragraphRange, attributedString, curElement, attributes);
+ applyStrikeThroughAttribute(paragraphRange, attributedString, curElement, attributes);
+ }
+ else {
+ // OK, now we loop until we find all the leaf elements in the
+ // range
+ while (pos < paragraphRange.endIndex) {
+ curElement = drillDownFromRoot(pos, rootElement);
+
+ // These are the mandatory attributes
+ final AttributeSet attributes = curElement.getAttributes();
+ final Color foreground = styleContext.getForeground(attributes);
+
+ attributedString.addAttribute(TextAttribute.FOREGROUND, foreground, Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), Math.min(paragraphRange.length(), curElement.getEndOffset()
+ - paragraphRange.startIndex));
+
+ final Font font = extractFont(styleContext, pos, rootElement, attributes);
+ applyFontAttribute(paragraphRange, attributedString, curElement, font);
+ applyBackgroundAttribute(styleContext, paragraphRange, attributedString, curElement, attributes);
+ applyUnderlineAttribute(paragraphRange, attributedString, curElement, attributes);
+ applyStrikeThroughAttribute(paragraphRange, attributedString, curElement, attributes);
+
+ // And set the position to the end of the given attribute
+ pos = curElement.getEndOffset();
+ }
+ }
+ }
+
+ recomputeLayout();
+ }
+
+ /**
+ * Returns the first leaf encountered by drilling into the document for the
+ * given position.
+ *
+ * @param pos position under which we're trying to find a leaf
+ * @param rootElement top most element in the document tree
+ *
+ * @return Leaf element that corresponds to the position provided in the
+ * document
+ */
+ private Element drillDownFromRoot(final int pos, final Element rootElement) {
+ // Before each pass, start at the root
+ Element curElement = rootElement;
+
+ // Now we descend the hierarchy until we get to a leaf
+ while (!curElement.isLeaf()) {
+ curElement = curElement.getElement(curElement.getElementIndex(pos));
+ }
+
+ return curElement;
+ }
+
+ private void applyFontAttribute(final RunInfo paragraphRange, final AttributedString attributedString,
+ final Element curElement, final Font font) {
+ if (font != null) {
+ attributedString.addAttribute(TextAttribute.FONT, font, Math.max(0, curElement.getStartOffset()
+ - paragraphRange.startIndex), Math.min(paragraphRange.endIndex - paragraphRange.startIndex,
+ curElement.getEndOffset() - paragraphRange.startIndex));
+ }
+ }
+
+ private void applyStrikeThroughAttribute(final RunInfo paragraphRange, final AttributedString attributedString,
+ final Element curElement, final AttributeSet attributes) {
+ final boolean strikethrough = StyleConstants.isStrikeThrough(attributes);
+ if (strikethrough) {
+ attributedString.addAttribute(TextAttribute.STRIKETHROUGH, Boolean.TRUE, Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), Math.min(paragraphRange.endIndex - paragraphRange.startIndex,
+ curElement.getEndOffset() - paragraphRange.startIndex));
+ }
+ }
+
+ private void applyUnderlineAttribute(final RunInfo paragraphRange, final AttributedString attributedString,
+ final Element curElement, final AttributeSet attributes) {
+ final boolean underline = StyleConstants.isUnderline(attributes);
+ if (underline) {
+ attributedString.addAttribute(TextAttribute.UNDERLINE, Boolean.TRUE, Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), Math.min(paragraphRange.endIndex - paragraphRange.startIndex,
+ curElement.getEndOffset() - paragraphRange.startIndex));
+ }
+ }
+
+ private void applyBackgroundAttribute(final StyleContext style, final RunInfo paragraphRange,
+ final AttributedString attributedString, final Element curElement, final AttributeSet attributes) {
+ if (attributes.isDefined(StyleConstants.Background)) {
+ final Color background = style.getBackground(attributes);
+ attributedString.addAttribute(TextAttribute.BACKGROUND, background, Math.max(0, curElement.getStartOffset()
+ - paragraphRange.startIndex), Math.min(paragraphRange.endIndex - paragraphRange.startIndex,
+ curElement.getEndOffset() - paragraphRange.startIndex));
+ }
+ }
+
+ private Font extractFont(final StyleContext style, final int pos, final Element rootElement,
+ final AttributeSet attributes) {
+ Font font = null;
+ if (attributes.isDefined(StyleConstants.FontSize) || attributes.isDefined(StyleConstants.FontFamily)) {
+ font = style.getFont(attributes);
+ }
+
+ if (font == null) {
+ if (document instanceof DefaultStyledDocument) {
+ font = extractFontFromDefaultStyledDocument((DefaultStyledDocument) document, style, pos, rootElement);
+ }
+ else {
+ font = style.getFont(rootElement.getAttributes());
+ }
+ }
+ return font;
+ }
+
+ private Font extractFontFromDefaultStyledDocument(final DefaultStyledDocument styledDocument,
+ final StyleContext style, final int pos, final Element rootElement) {
+ Font font = style.getFont(styledDocument.getCharacterElement(pos).getAttributes());
+ if (font == null) {
+ font = style.getFont(styledDocument.getParagraphElement(pos).getAttributes());
+ if (font == null) {
+ font = style.getFont(rootElement.getAttributes());
+ }
+ }
+ return font;
+ }
+
+ private ArrayList extractParagraphRanges(final String documentString) {
+ // The paragraph start and end indices
+ final ArrayList paragraphRanges = new ArrayList();
+
+ // The current position in the specified range
+ int pos = 0;
+
+ final StringTokenizer tokenizer = new StringTokenizer(documentString, "\n", true);
+
+ // lastNewLine is used to detect the case when two newlines follow
+ // in direct succession
+ // & lastNewLine should be true to start in case the first character
+ // is a newline
+ boolean lastNewLine = true;
+
+ while (tokenizer.hasMoreTokens()) {
+ final String token = tokenizer.nextToken();
+
+ // If the token
+ if (token.equals("\n")) {
+ if (lastNewLine) {
+ stringContents.add(new AttributedString(" "));
+ paragraphRanges.add(new RunInfo(pos, pos + 1));
+ }
+
+ pos = pos + 1;
+
+ lastNewLine = true;
+ }
+ // If the token is empty - create an attributed string with a
+ // single space since LineBreakMeasurers don't work with an empty
+ // string
+ // - note that this case should only arise if the document is empty
+ else if (token.equals("")) {
+ stringContents.add(new AttributedString(" "));
+ paragraphRanges.add(new RunInfo(pos, pos));
+
+ lastNewLine = false;
+ }
+ // This is the normal case - where we have some text
+ else {
+ stringContents.add(new AttributedString(token));
+ paragraphRanges.add(new RunInfo(pos, pos + token.length()));
+
+ // Increment the position
+ pos = pos + token.length();
+
+ lastNewLine = false;
+ }
+ }
+
+ // Add one more newline if the last character was a newline
+ if (lastNewLine) {
+ stringContents.add(new AttributedString(" "));
+ paragraphRanges.add(new RunInfo(pos, pos + 1));
+ }
+
+ return paragraphRanges;
+ }
+
+ /**
+ * Compute the bounds of the text wrapped by this node. The text layout is
+ * wrapped based on the bounds of this node. If the shrinkBoundsToFit
+ * parameter is true then after the text has been laid out the bounds of
+ * this node are shrunk to fit around those text bounds.
+ */
+ public void recomputeLayout() {
+ if (stringContents == null) {
+ return;
+ }
+
+ final ArrayList linesList = new ArrayList();
+
+ double textWidth = 0;
+ double textHeight = 0;
+
+ final Iterator contentIterator = stringContents.iterator();
+
+ while (contentIterator.hasNext()) {
+ final AttributedString ats = (AttributedString) contentIterator.next();
+ final AttributedCharacterIterator itr = ats.getIterator();
+
+ LineBreakMeasurer measurer;
+ ArrayList breakList = null;
+
+ measurer = new LineBreakMeasurer(itr, SWING_FRC);
+ breakList = extractLineBreaks(itr, measurer);
+
+ measurer = new LineBreakMeasurer(itr, PPaintContext.RENDER_QUALITY_HIGH_FRC);
+
+ // Need to change the lineinfo data structure to know about multiple
+ // text layouts per line
+
+ LineInfo lineInfo = null;
+ boolean newLine = true;
+ double lineWidth = 0;
+ while (measurer.getPosition() < itr.getEndIndex()) {
+ TextLayout aTextLayout = null;
+
+ if (newLine) {
+ newLine = false;
+
+ final double lineHeight = calculateLineHeightFromLineInfo(lineInfo);
+
+ textHeight = textHeight + lineHeight;
+ textWidth = Math.max(textWidth, lineWidth);
+
+ // Now create a new line
+ lineInfo = new LineInfo();
+ linesList.add(lineInfo);
+ }
+
+ final int lineEnd = ((Integer) breakList.get(0)).intValue();
+ if (lineEnd <= itr.getRunLimit()) {
+ breakList.remove(0);
+ newLine = true;
+ }
+
+ aTextLayout = measurer.nextLayout(Float.MAX_VALUE, Math.min(lineEnd, itr.getRunLimit()), false);
+
+ final SegmentInfo sInfo = new SegmentInfo();
+ sInfo.font = (Font) itr.getAttribute(TextAttribute.FONT);
+ sInfo.foreground = (Color) itr.getAttribute(TextAttribute.FOREGROUND);
+ sInfo.background = (Color) itr.getAttribute(TextAttribute.BACKGROUND);
+ sInfo.underline = (Boolean) itr.getAttribute(TextAttribute.UNDERLINE);
+ sInfo.layout = aTextLayout;
+
+ final FontMetrics metrics = StyleContext.getDefaultStyleContext().getFontMetrics(
+ (Font) itr.getAttribute(TextAttribute.FONT));
+ lineInfo.maxAscent = Math.max(lineInfo.maxAscent, metrics.getMaxAscent());
+ lineInfo.maxDescent = Math.max(lineInfo.maxDescent, metrics.getMaxDescent());
+ lineInfo.leading = Math.max(lineInfo.leading, metrics.getLeading());
+
+ lineInfo.segments.add(sInfo);
+
+ itr.setIndex(measurer.getPosition());
+ lineWidth = lineWidth + aTextLayout.getAdvance();
+ }
+
+ final double lineHeight = calculateLineHeightFromLineInfo(lineInfo);
+ textHeight = textHeight + lineHeight;
+ textWidth = Math.max(textWidth, lineWidth);
+ }
+
+ lines = (LineInfo[]) linesList.toArray(new LineInfo[linesList.size()]);
+
+ constrainDimensionsIfNeeded(textWidth, textHeight);
+ }
+
+ /**
+ * @param lineInfo
+ * @return
+ */
+ private double calculateLineHeightFromLineInfo(final LineInfo lineInfo) {
+ final double lineHeight;
+ if (lineInfo == null) {
+ lineHeight = 0;
+ }
+ else {
+ lineHeight = lineInfo.maxAscent + lineInfo.maxDescent + lineInfo.leading;
+ }
+ return lineHeight;
+ }
+
+ private void constrainDimensionsIfNeeded(final double textWidth, final double textHeight) {
+ if (!constrainWidthToTextWidth && !constrainHeightToTextHeight) {
+ return;
+ }
+
+ double newWidth = getWidth();
+ double newHeight = getHeight();
+
+ if (constrainWidthToTextWidth) {
+ newWidth = textWidth + insets.left + insets.right;
+ }
+
+ if (constrainHeightToTextHeight) {
+ newHeight = Math.max(textHeight, getInitialFontHeight()) + insets.top + insets.bottom;
+ }
+
+ super.setBounds(getX(), getY(), newWidth, newHeight);
+ }
+
+ // Because swing doesn't use fractional font metrics by default, we use
+ // LineBreakMeasurer to find out where Swing is going to break them
+ private ArrayList extractLineBreaks(final AttributedCharacterIterator itr, final LineBreakMeasurer measurer) {
+ ArrayList breakList;
+ breakList = new ArrayList();
+ while (measurer.getPosition() < itr.getEndIndex()) {
+ if (constrainWidthToTextWidth) {
+ measurer.nextLayout(Float.MAX_VALUE);
+ }
+ else {
+ measurer.nextLayout((float) Math.ceil(getWidth() - insets.left - insets.right));
+ }
+
+ breakList.add(new Integer(measurer.getPosition()));
+ }
+ return breakList;
+ }
+
+ /**
+ * Get the height of the font at the beginning of the document.
+ *
+ * @return height of font at the start of the document.
+ */
+ public double getInitialFontHeight() {
+
+ // Small assumption here that there is one root element - can fix
+ // for more general support later
+ final Element rootElement = document.getDefaultRootElement();
+ final Element curElement = drillDownFromRoot(0, rootElement);
+ final StyleContext context = StyleContext.getDefaultStyleContext();
+ final Font font = context.getFont(curElement.getAttributes());
+
+ final FontMetrics curFM = context.getFontMetrics(font);
+
+ return curFM.getMaxAscent() + curFM.getMaxDescent() + curFM.getLeading();
+ }
+
+ /** {@inheritDoc} */
+ protected void paint(final PPaintContext paintContext) {
+ if (lines == null || lines.length == 0) {
+ return;
+ }
+
+ final float x = (float) (getX() + insets.left);
+ float y = (float) (getY() + insets.top);
+ final float bottomY = (float) (getY() + getHeight() - insets.bottom);
+
+ final Graphics2D g2 = paintContext.getGraphics();
+
+ if (getPaint() != null) {
+ g2.setPaint(getPaint());
+ g2.fill(getBoundsReference());
+ }
+
+ float curX;
+ LineInfo lineInfo;
+ for (int i = 0; i < lines.length; i++) {
+ lineInfo = lines[i];
+ y += lineInfo.maxAscent;
+ curX = x;
+
+ if (bottomY < y) {
+ return;
+ }
+
+ for (int j = 0; j < lineInfo.segments.size(); j++) {
+ final SegmentInfo sInfo = (SegmentInfo) lineInfo.segments.get(j);
+ final float width = sInfo.layout.getAdvance();
+
+ if (sInfo.background != null) {
+ g2.setPaint(sInfo.background);
+ g2.fill(new Rectangle2D.Double(curX, y - lineInfo.maxAscent, width, lineInfo.maxAscent
+ + lineInfo.maxDescent + lineInfo.leading));
+ }
+
+ sInfo.applyFont(g2);
+
+ // Manually set the paint - this is specified in the
+ // AttributedString but seems to be
+ // ignored by the TextLayout. To handle multiple colors we
+ // should be breaking up the lines
+ // but that functionality can be added later as needed
+ g2.setPaint(sInfo.foreground);
+ sInfo.layout.draw(g2, curX, y);
+
+ // Draw the underline and the strikethrough after the text
+ if (sInfo.underline != null) {
+ paintLine.setLine(x, y + lineInfo.maxDescent / 2, x + width, y + lineInfo.maxDescent / 2);
+ g2.draw(paintLine);
+ }
+
+ curX = curX + width;
+ }
+
+ y += lineInfo.maxDescent + lineInfo.leading;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void fullPaint(final PPaintContext paintContext) {
+ if (!editing) {
+ super.fullPaint(paintContext);
+ }
+ }
+
+ /**
+ * Set whether this node is current in editing mode.
+ *
+ * @param editing value to set editing flag
+ */
+ public void setEditing(final boolean editing) {
+ this.editing = editing;
+ }
+
+ /**
+ * Whether node is currently in editing state.
+ *
+ * @return true if node is currently editing
+ */
+ public boolean isEditing() {
+ return editing;
+ }
+
+ /**
+ * Set the insets of the text.
+ *
+ * @param insets desired insets
+ */
+ public void setInsets(final Insets insets) {
+ if (insets != null) {
+ this.insets.left = insets.left;
+ this.insets.right = insets.right;
+ this.insets.top = insets.top;
+ this.insets.bottom = insets.bottom;
+
+ recomputeLayout();
+ }
+ }
+
+ /**
+ * Get the insets of the text.
+ *
+ * @return current text insets
+ */
+ public Insets getInsets() {
+ return (Insets) insets.clone();
+ }
+
+ /** {@inheritDoc} */
+ public boolean setBounds(final double x, final double y, final double width, final double height) {
+ if (document == null || !super.setBounds(x, y, width, height)) {
+ return false;
+ }
+
+ recomputeLayout();
+ return true;
+ }
+
+ /**
+ * Simple class to represent an range within the document.
+ */
+ protected static class RunInfo {
+ private int startIndex;
+ private int endIndex;
+
+ /**
+ * Constructs a RunInfo representing the range within the document from
+ * runStart to runLimit.
+ *
+ * @param runStart starting index of the range
+ * @param runLimit ending index of the range
+ */
+ public RunInfo(final int runStart, final int runLimit) {
+ startIndex = runStart;
+ endIndex = runLimit;
+ }
+
+ /**
+ * Returns whether the run is empty.
+ *
+ * @return true is run is empty
+ */
+ public boolean isEmpty() {
+ return startIndex == endIndex;
+ }
+
+ /**
+ * Returns the length of the run.
+ *
+ * @return length of run
+ */
+ public int length() {
+ return endIndex - startIndex;
+ }
+ }
+
+ /**
+ * The info for rendering and computing the bounds of a line.
+ */
+ protected static class LineInfo {
+ /** Segments which make up this line's formatting segments. */
+ public List segments;
+
+ /** Maximum of the line segments' ascents. */
+ public double maxAscent;
+
+ /** Maximum of the line segments' descents. */
+ public double maxDescent;
+
+ /** Leading space at front of line segment. */
+ public double leading;
+
+ /**
+ * Creates a LineInfo that contains no segments.
+ */
+ public LineInfo() {
+ segments = new ArrayList();
+ }
+ }
+
+ /**
+ * Encapsulates information about a particular LineSegment.
+ */
+ protected static class SegmentInfo {
+ /** Text Layout applied to the segment. */
+ public TextLayout layout;
+
+ /** Font being used to render the segment. */
+ public Font font;
+
+ /** Foreground (text) color of the segment. */
+ public Color foreground;
+
+ /** Background color of the segment. */
+ public Color background;
+
+ /** Whether the segment is underlined. */
+ public Boolean underline;
+
+ /** Construct a segment with null properties. */
+ public SegmentInfo() {
+ }
+
+ /**
+ * Applies this particular SegmentInfo's font to the graphics object
+ * passed in.
+ *
+ * @param g2 will have the font of this segment applied
+ */
+ public void applyFont(final Graphics2D g2) {
+ if (font != null) {
+ g2.setFont(font);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolox/nodes/package.html b/src/main/java/edu/umd/cs/piccolox/nodes/package.html
new file mode 100644
index 0000000..78c437c
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/nodes/package.html
@@ -0,0 +1,34 @@
+
+
+
+This package contains additional nodes that may be useful for Piccolo applications.
+ + diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PComboBox.java b/src/main/java/edu/umd/cs/piccolox/pswing/PComboBox.java new file mode 100644 index 0000000..29ed1f5 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/pswing/PComboBox.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.pswing; + +import java.awt.Rectangle; +import java.awt.geom.Rectangle2D; +import java.io.Serializable; +import java.util.Vector; + +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.plaf.basic.BasicComboBoxUI; +import javax.swing.plaf.basic.BasicComboPopup; +import javax.swing.plaf.basic.ComboPopup; + +/** + * The PComboBox is used instead of a JComboBox in a Piccolo scene graph. + * This PComboBox won't work properly if it is located in an abnormal hierarchy + * of Cameras. Support is provided for only one (or zero) view transforms. + *+ * A ComboBox for use in Piccolo. This still has an associated JPopupMenu (which + * is always potentially heavyweight depending on component location relative to + * containing window borders.) However, this ComboBox places the PopupMenu + * component of the ComboBox in the appropriate position relative to the + * permanent part of the ComboBox. The PopupMenu is never transformed. + *
+ *+ * This class was not designed for subclassing. If different behavior is + * required, it seems more appropriate to subclass JComboBox directly using this + * class as a model. + *
+ *+ * NOTE: There is currently a known bug, namely, if the ComboBox receives focus + * through 'tab' focus traversal and the keyboard is used to interact with the + * ComboBox, there may be unexpected results. + *
+ *+ * Warning: Serialized objects of this class will not be compatible with + * future Piccolo releases. The current serialization support is appropriate for + * short term storage or RMI between applications running the same version of + * Piccolo. A future release of Piccolo will provide support for long term + * persistence. + *
+ * + * @author Lance Good + */ +public class PComboBox extends JComboBox implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + private PSwing pSwing; + private PSwingCanvas canvas; + + /** + * Creates a PComboBox that takes its items from an existing ComboBoxModel. + * + * @param model The ComboBoxModel from which the list will be created + */ + public PComboBox(final ComboBoxModel model) { + super(model); + init(); + } + + /** + * Creates a PComboBox that contains the elements in the specified array. + * + * @param items The items to populate the PComboBox list + */ + public PComboBox(final Object[] items) { + super(items); + init(); + } + + /** + * Creates a PComboBox that contains the elements in the specified Vector. + * + * @param items The items to populate the PComboBox list + */ + public PComboBox(final Vector items) { + super(items); + init(); + } + + /** + * Create an empty PComboBox. + */ + public PComboBox() { + super(); + init(); + } + + /** + * Substitute our UI for the default. + */ + private void init() { + setUI(new PBasicComboBoxUI()); + } + + /** + * Clients must set the PSwing and PSwingCanvas environment for this + * PComboBox to work properly. + * + * @param pSwingNode node that this PComboBox is attached to + * @param canvasEnvirnoment canvas on which the pSwing node is embedded + */ + public void setEnvironment(final PSwing pSwingNode, final PSwingCanvas canvasEnvirnoment) { + this.pSwing = pSwingNode; + this.canvas = canvasEnvirnoment; + } + + /** + * The substitute look and feel - used to capture the mouse events on the + * arrowButton and the component itself and to create our PopupMenu rather + * than the default. + */ + protected class PBasicComboBoxUI extends BasicComboBoxUI { + + /** + * Create our Popup instead of swing's default. + * + * @return a new ComboPopup + */ + protected ComboPopup createPopup() { + final PBasicComboPopup popup = new PBasicComboPopup(comboBox); + popup.getAccessibleContext().setAccessibleParent(comboBox); + return popup; + } + } + + /** + * The substitute ComboPopupMenu that places itself correctly in Piccolo2d. + */ + protected class PBasicComboPopup extends BasicComboPopup { + private static final long serialVersionUID = 1L; + + /** + * Creates a PBasicComboPopup that will position itself correctly in + * relation to the provided JComboBox. + * + * @param combo The associated ComboBox + */ + public PBasicComboPopup(final JComboBox combo) { + super(combo); + } + + /** + * Computes the bounds for the Popup in Piccolo2D if a PMouseEvent has + * been received. Otherwise, it uses the default algorithm for placing + * the popup. + * + * @param px corresponds to the x coordinate of the popup + * @param py corresponds to the y coordinate of the popup + * @param pw corresponds to the width of the popup + * @param ph corresponds to the height of the popup + * @return The bounds for the PopupMenu + */ + protected Rectangle computePopupBounds(final int px, final int py, final int pw, final int ph) { + final Rectangle2D r = getNodeBoundsInCanvas(); + final Rectangle sup = super.computePopupBounds(px, py, pw, ph); + return new Rectangle((int) r.getX(), (int) r.getMaxY(), (int) sup.getWidth(), (int) sup.getHeight()); + } + } + + private Rectangle2D getNodeBoundsInCanvas() { + if (pSwing == null || canvas == null) { + throw new RuntimeException( + "PComboBox.setEnvironment( swing, pCanvas );//has to be done manually at present"); + } + Rectangle2D r1c = new Rectangle2D.Double(pSwing.getX(), pSwing.getY(), getWidth(), getHeight()); + pSwing.localToGlobal(r1c); + canvas.getCamera().globalToLocal(r1c); + r1c = canvas.getCamera().getViewTransform().createTransformedShape(r1c).getBounds2D(); + return r1c; + } + + /** + * Returns the associated PSwing node. + * + * @return associated PSwing node + */ + public PSwing getPSwing() { + return pSwing; + } + + /** + * Returns the canvas on which the PSwing node is embedded. + * + * @return canvas on which the PSwing node is embedded + */ + public PSwingCanvas getCanvas() { + return canvas; + } + +} diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PSwing.java b/src/main/java/edu/umd/cs/piccolox/pswing/PSwing.java new file mode 100644 index 0000000..1ec5616 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/pswing/PSwing.java @@ -0,0 +1,743 @@ +/* + * Copyright (c) 2008, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.pswing; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PLayer; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.util.PBounds; +import edu.umd.cs.piccolo.util.PPaintContext; + +import javax.swing.JComponent; +import javax.swing.RepaintManager; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.event.ContainerAdapter; +import java.awt.event.ContainerEvent; +import java.awt.event.ContainerListener; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; + +/* + This message was sent to Sun on August 27, 1999 + + ----------------------------------------------- + + We are currently developing Piccolo, a "scenegraph" for use in 2D graphics. + One of our ultimate goals is to support Swing lightweight components + within Piccolo, whose graphical space supports arbitray affine transforms. + The challenge in this pursuit is getting the components to respond and + render properly though not actually displayed in a standard Java component + hierarchy. + + + The first issues involved making the Swing components focusable and + showing. This was accomplished by adding the Swing components to a 0x0 + JComponent which was in turn added to our main Piccolo application component. + To our good fortune, a Java component is showing merely if it and its + ancestors are showing and not based on whether it is ACTUALLY visible. + Likewise, focus in a JComponent depends merely on the component's + containing window having focus. + + + The second issue involved capturing the repaint calls on a Swing + component. Normally, for a repaint and the consequent call to + paintImmediately, a Swing component obtains the Graphics object necessary + to render itself through the Java component heirarchy. However, for Piccolo + we would like the component to render using a Graphics object that Piccolo + may have arbitrarily transformed in some way. By capturing in the + RepaintManager the repaint calls made on our special Swing components, we + are able to redirect the repaint requests through the Piccolo architecture to + put the Graphics in its proper context. Unfortunately, this means that + if the Swing component contains other Swing components, then any repaint + requests made by one of these nested components must go through + the Piccolo architecture then through the top level Swing component + down to the nested Swing component. This normally doesn't cause a + problem. However, if calling paint on one of these nested + children causes a call to repaint then an infinite loop ensues. This does + in fact happen in the Swing components that use cell renderers. Before + the cell renderer is painted, it is invalidated and consequently + repainted. We solved this problem by putting a lock on repaint calls for + a component while that component is painting. (A similar problem faced + the Swing team over this same issue. They solved it by inserting a + CellRendererPane to capture the renderer's invalidate calls.) + + + Another issue arose over the forwarding of mouse events to the Swing + components. Since our Swing components are not actually displayed on + screen in the standard manner, we must manually dispatch any MouseEvents + we want the component to receive. Hence, we needed to find the deepest + visible component at a particular location that accepts MouseEvents. + Finding the deepest visible component at a point was achieved with the + "findComponentAt" method in java.awt.Container. With the + "getListeners(Class listenerType)" method added in JDK1.3 Beta we are able + to determine if the component has any Mouse Listeners. However, we haven't + yet found a way to determine if MouseEvents have been specifically enabled + for a component. The package private method "eventEnabled" in + java.awt.Component does exactly what we want but is, of course, + inaccessible. In order to dispatch events correctly we would need a + public accessor to the method "boolean eventEnabled(AWTEvent)" in + java.awt.Component. + + + Still another issue involves the management of cursors when the mouse is + over a Swing component in our application. To the Java mechanisms, the + mouse never appears to enter the bounds of the Swing components since they + are contained by a 0x0 JComponent. Hence, we must manually change the + cursor when the mouse enters one of the Swing components in our + application. This generally works but becomes a problem if the Swing + component's cursor changes while we are over that Swing component (for + instance, if you resize a Table Column). In order to manage cursors + properly, we would need setCursor to fire property change events. + + + With the above fixes, most Swing components work. The only Swing + components that are definitely broken are ToolTips and those that rely on + JPopupMenu. In order to implement ToolTips properly, we would need to have + a method in ToolTipManager that allows us to set the current manager, as + is possible with RepaintManager. In order to implement JPopupMenu, we + will likely need to re-implement JPopupMenu to function in Piccolo2d with + a transformed Graphics and to insert itself in the proper place in the + Piccolo2d scenegraph. + + */ + +/** + * PSwing is used to add Swing Components to a Piccolo2D canvas. + *+ * Example: adding a swing JButton to a PCanvas: + * + *
+ * PSwingCanvas canvas = new PSwingCanvas(); + * JButton button = new JButton("Button"); + * swing = new PSwing(canvas, button); + * canvas.getLayer().addChild(swing); + *+ * + *
+ * NOTE: PSwing has the current limitation that it does not listen for Container + * events. This is only an issue if you create a PSwing and later add Swing + * components to the PSwing's component hierarchy that do not have double + * buffering turned off or have a smaller font size than the minimum font size + * of the original PSwing's component hierarchy. + *
+ *+ * For instance, the following bit of code will give unexpected results: + * + *
+ * JPanel panel = new JPanel(); + * PSwing swing = new PSwing(panel); + * JPanel newChild = new JPanel(); + * newChild.setDoubleBuffered(true); + * panel.add(newChild); + *+ * + * + *
+ * NOTE: PSwing cannot be correctly interacted with through multiple cameras. + * There is no support for it yet. + *
+ *+ * NOTE: PSwing is java.io.Serializable. + *
+ *+ * Warning: Serialized objects of this class will not be compatible with + * future Piccolo releases. The current serialization support is appropriate for + * short term storage or RMI between applications running the same version of + * Piccolo. A future release of Piccolo will provide support for long term + * persistence. + *
+ * + * @author Sam R. Reid + * @author Chris Malley (cmalley@pixelzoom.com) + * @author Benjamin B. Bederson + * @author Lance E. Good + * + */ +public class PSwing extends PNode implements Serializable, PropertyChangeListener { + /** Default serial version UID. */ + private static final long serialVersionUID = 1L; + + /** Key for this object in the Swing component's client properties. */ + public static final String PSWING_PROPERTY = "PSwing"; + + /** Temporary repaint bounds. */ + private static final PBounds TEMP_REPAINT_BOUNDS2 = new PBounds(); + + /** For use when buffered painting is enabled. */ + private static final Color BUFFER_BACKGROUND_COLOR = new Color(0, 0, 0, 0); + + private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); + + /** Default Greek threshold,0.3d
. */
+ private static final double DEFAULT_GREEK_THRESHOLD = 0.3d;
+
+ /** The cutoff at which the Swing component is rendered greek. */
+ private double greekThreshold = DEFAULT_GREEK_THRESHOLD;
+
+ /** Swing component for this Swing node. */
+ private JComponent component = null;
+
+ /**
+ * Whether or not to use buffered painting.
+ * @see #paint(java.awt.Graphics2D)
+ */
+ private boolean useBufferedPainting = false;
+
+ /** Used when buffered painting is enabled. */
+ private BufferedImage buffer;
+
+ /** Minimum font size. */
+ private double minFontSize = Double.MAX_VALUE;
+
+ /**
+ * Default stroke, new BasicStroke()
. Cannot be made static
+ * because BasicStroke is not serializable. Should not be null.
+ */
+ private Stroke defaultStroke = new BasicStroke();
+
+ /**
+ * Default font, 12 point "SansSerif"
. Will be made final in
+ * version 2.0.
+ */
+ // public static final Font DEFAULT_FONT = new Font(Font.SANS_SERIF,
+ // Font.PLAIN, 12); jdk 1.6+
+ private static final Font DEFAULT_FONT = new Font("Serif", Font.PLAIN, 12);
+
+ /** Swing canvas for this swing node. */
+ private PSwingCanvas canvas;
+
+ /**
+ * Used to keep track of which nodes we've attached listeners to since no
+ * built in support in PNode.
+ */
+ private final ArrayList listeningTo = new ArrayList();
+
+ /** The parent listener for camera/canvas changes. */
+ private final PropertyChangeListener parentListener = new PropertyChangeListener() {
+ /** {@inheritDoc} */
+ public void propertyChange(final PropertyChangeEvent evt) {
+ final PNode parent = (PNode) evt.getNewValue();
+ clearListeners((PNode) evt.getOldValue());
+ if (parent == null) {
+ updateCanvas(null);
+ }
+ else {
+ listenForCanvas(parent);
+ }
+ }
+
+ /**
+ * Clear out all the listeners registered to make sure there are no
+ * stray references.
+ *
+ * @param fromParent Parent to start with for clearing listeners
+ */
+ private void clearListeners(final PNode fromParent) {
+ if (fromParent != null && isListeningTo(fromParent)) {
+ fromParent.removePropertyChangeListener(PNode.PROPERTY_PARENT, parentListener);
+ listeningTo.remove(fromParent);
+ clearListeners(fromParent.getParent());
+ }
+ }
+
+ };
+
+ /**
+ * Listens to container nodes for changes to its contents. Any additions
+ * will automatically have double buffering turned off.
+ */
+ private final ContainerListener doubleBufferRemover = new ContainerAdapter() {
+ public void componentAdded(final ContainerEvent event) {
+ Component childComponent = event.getChild();
+ if (childComponent != null && childComponent instanceof JComponent) {
+ disableDoubleBuffering(((JComponent) childComponent));
+ }
+ };
+
+ /**
+ * Disables double buffering on every component in the hierarchy of the
+ * targetComponent.
+ *
+ * I'm assuming that the intent of the is method is that it should be
+ * called explicitly by anyone making changes to the hierarchy of the
+ * Swing component graph.
+ */
+ private void disableDoubleBuffering(final JComponent targetComponent) {
+ targetComponent.setDoubleBuffered(false);
+ for (int i = 0; i < targetComponent.getComponentCount(); i++) {
+ final Component c = targetComponent.getComponent(i);
+ if (c instanceof JComponent) {
+ disableDoubleBuffering((JComponent) c);
+ }
+ }
+ }
+ };
+
+ /**
+ * Create a new visual component wrapper for the specified Swing component.
+ *
+ * @param component Swing component to be wrapped
+ */
+ public PSwing(final JComponent component) {
+ this.component = component;
+ component.putClientProperty(PSWING_PROPERTY, this);
+ initializeComponent(component);
+
+ component.revalidate();
+ updateBounds();
+ listenForCanvas(this);
+ }
+
+ /**
+ * @deprecated by {@link #PSwing(JComponent)}
+ *
+ * @param swingCanvas canvas on which the PSwing node will be embedded
+ * @param component not used
+ */
+ public PSwing(final PSwingCanvas swingCanvas, final JComponent component) {
+ this(component);
+ }
+
+ /**
+ * If true {@link PSwing} will paint the {@link JComponent} to a buffer with no graphics
+ * transformations applied and then paint the buffer to the target transformed
+ * graphics context. On some platforms (such as Mac OS X) rendering {@link JComponent}s to
+ * a transformed context is slow. Enabling buffered painting gives a significant performance
+ * boost on these platforms; however, at the expense of a lower-quality drawing result at larger
+ * scales.
+ * @since 1.3.1
+ * @param useBufferedPainting true if this {@link PSwing} should use buffered painting
+ */
+ public void setUseBufferedPainting(final boolean useBufferedPainting) {
+ this.useBufferedPainting = useBufferedPainting;
+ }
+
+ public boolean isUseBufferedPainting() {
+ return this.useBufferedPainting;
+ }
+
+ /**
+ * Ensures the bounds of the underlying component are accurate, and sets the
+ * bounds of this PNode.
+ */
+ public void updateBounds() {
+ /*
+ * Need to explicitly set the component's bounds because
+ * the component's parent (PSwingCanvas.ChildWrapper) has no layout manager.
+ */
+ if (componentNeedsResizing()) {
+ updateComponentSize();
+ }
+ setBounds(0, 0, component.getPreferredSize().width, component.getPreferredSize().height);
+ }
+
+ /**
+ * Since the parent ChildWrapper has no layout manager, it is the responsibility of this PSwing
+ * to make sure the component has its bounds set properly, otherwise it will not be drawn properly.
+ * This method sets the bounds of the component to be equal to its preferred size.
+ */
+ private void updateComponentSize() {
+ component.setBounds(0, 0, component.getPreferredSize().width, component.getPreferredSize().height);
+ }
+
+ /**
+ * Determines whether the component should be resized, based on whether its actual width and height
+ * differ from its preferred width and height.
+ * @return true if the component should be resized.
+ */
+ private boolean componentNeedsResizing() {
+ return component.getWidth() != component.getPreferredSize().width || component.getHeight() != component.getPreferredSize().height;
+ }
+
+ /**
+ * Paints the PSwing on the specified renderContext. Also determines if
+ * the Swing component should be rendered normally or as a filled rectangle (greeking).
+ *
+ * The transform, clip, and composite will be set appropriately when this
+ * object is rendered. It is up to this object to restore the transform,
+ * clip, and composite of the Graphics2D if this node changes any of them.
+ * However, the color, font, and stroke are unspecified by Piccolo. This
+ * object should set those things if they are used, but they do not need to
+ * be restored.
+ *
+ * @param renderContext Contains information about current render.
+ */
+ public void paint(final PPaintContext renderContext) {
+ if (componentNeedsResizing()) {
+ updateComponentSize();
+ component.validate();
+ }
+ final Graphics2D g2 = renderContext.getGraphics();
+
+ //Save Stroke and Font for restoring.
+ Stroke originalStroke = g2.getStroke();
+ Font originalFont = g2.getFont();
+
+ g2.setStroke(defaultStroke);
+ g2.setFont(DEFAULT_FONT);
+
+ if (shouldRenderGreek(renderContext)) {
+ paintAsGreek(g2);
+ }
+ else {
+ paint(g2);
+ }
+
+ //Restore the stroke and font on the Graphics2D
+ g2.setStroke(originalStroke);
+ g2.setFont(originalFont);
+ }
+
+ /**
+ * Return true if this Swing node should render as greek given the specified
+ * paint context.
+ *
+ * @param paintContext paint context
+ * @return true if this Swing node should render as greek given the
+ * specified paint context
+ */
+ protected boolean shouldRenderGreek(final PPaintContext paintContext) {
+ return paintContext.getScale() < greekThreshold || minFontSize * paintContext.getScale() < 0.5;
+ }
+
+ /**
+ * Paints the Swing component as greek. This method assumes that the stroke has been set beforehand.
+ *
+ * @param g2 The graphics used to render the filled rectangle
+ */
+ public void paintAsGreek(final Graphics2D g2) {
+ //Save original color for restoring painting as greek.
+ Color originalColor = g2.getColor();
+
+ if (component.getBackground() != null) {
+ g2.setColor(component.getBackground());
+ }
+ g2.fill(getBounds());
+
+ if (component.getForeground() != null) {
+ g2.setColor(component.getForeground());
+ }
+ g2.draw(getBounds());
+
+ //Restore original color on the Graphics2D
+ g2.setColor(originalColor);
+ }
+
+ /** {@inheritDoc} */
+ public void setVisible(final boolean visible) {
+ super.setVisible(visible);
+
+ if (component.isVisible() != visible) {
+ component.setVisible(visible);
+ }
+ }
+
+ /**
+ * Remove from the SwingWrapper; throws an exception if no canvas is
+ * associated with this PSwing.
+ */
+ public void removeFromSwingWrapper() {
+ if (canvas != null && isComponentSwingWrapped()) {
+ canvas.getSwingWrapper().remove(component);
+ }
+ }
+
+ private boolean isComponentSwingWrapped() {
+ return Arrays.asList(canvas.getSwingWrapper().getComponents()).contains(component);
+ }
+
+ /**
+ * Renders the wrapped component to the graphics context provided.
+ *
+ * @param g2 graphics context for rendering the JComponent
+ */
+ public void paint(final Graphics2D g2) {
+ if (component.getBounds().isEmpty()) {
+ // The component has not been initialized yet.
+ return;
+ }
+
+ final PSwingRepaintManager manager = (PSwingRepaintManager) RepaintManager.currentManager(component);
+ manager.lockRepaint(component);
+
+ final RenderingHints oldHints = g2.getRenderingHints();
+
+ if (useBufferedPainting) {
+ Graphics2D bufferedGraphics = getBufferedGraphics(g2);
+ component.paint(bufferedGraphics);
+ g2.drawRenderedImage(buffer, IDENTITY_TRANSFORM);
+ } else {
+ g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
+ component.paint(g2);
+ }
+
+ g2.setRenderingHints(oldHints);
+
+ manager.unlockRepaint(component);
+ }
+
+ private Graphics2D getBufferedGraphics(Graphics2D source) {
+ final Graphics2D bufferedGraphics;
+ if(!isBufferValid()) {
+ // Get the graphics context associated with a new buffered image.
+ // Use TYPE_INT_ARGB_PRE so that transparent components look good on Windows.
+ buffer = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE);
+ bufferedGraphics = buffer.createGraphics();
+ }
+ else {
+ // Use the graphics context associated with the existing buffered image
+ bufferedGraphics = buffer.createGraphics();
+ // Clear the buffered image to prevent artifacts on Macintosh
+ bufferedGraphics.setBackground(BUFFER_BACKGROUND_COLOR);
+ bufferedGraphics.clearRect(0, 0, component.getWidth(), component.getHeight());
+ }
+ bufferedGraphics.setRenderingHints(source.getRenderingHints());
+ return bufferedGraphics;
+ }
+
+ /**
+ * Tells whether the buffer for the image of the Swing components
+ * is currently valid.
+ *
+ * @return true if the buffer is currently valid
+ */
+ private boolean isBufferValid() {
+ return !(buffer == null || buffer.getWidth() != component.getWidth() || buffer.getHeight() != component.getHeight());
+ }
+
+ /**
+ * Repaints the specified portion of this visual component. Note that the
+ * input parameter may be modified as a result of this call.
+ *
+ * @param repaintBounds bounds that need repainting
+ */
+ public void repaint(final PBounds repaintBounds) {
+ final Shape sh = getTransform().createTransformedShape(repaintBounds);
+ TEMP_REPAINT_BOUNDS2.setRect(sh.getBounds2D());
+ repaintFrom(TEMP_REPAINT_BOUNDS2, this);
+ }
+
+ /**
+ * Returns the Swing component that this visual component wraps.
+ *
+ * @return The Swing component wrapped by this PSwing node
+ */
+ public JComponent getComponent() {
+ return component;
+ }
+
+ /**
+ * We need to turn off double buffering of Swing components within Piccolo
+ * since all components contained within a native container use the same
+ * buffer for double buffering. With normal Swing widgets this is fine, but
+ * for Swing components within Piccolo this causes problems. This function
+ * recurses the component tree rooted at c, and turns off any double
+ * buffering in use. It also updates the minimum font size based on the font
+ * size of c and adds a property change listener to listen for changes to
+ * the font.
+ *
+ * @param c The Component to be recursively unDoubleBuffered
+ */
+ private void initializeComponent(final Component c) {
+
+ if (c.getFont() != null) {
+ minFontSize = Math.min(minFontSize, c.getFont().getSize());
+ }
+ c.addPropertyChangeListener("font", this);
+
+ if (c instanceof Container) {
+ initializeChildren((Container) c);
+ ((Container) c).addContainerListener(doubleBufferRemover);
+ }
+
+ if (c instanceof JComponent) {
+ ((JComponent) c).setDoubleBuffered(false);
+ }
+ }
+
+ private void initializeChildren(final Container c) {
+ final Component[] children = c.getComponents();
+ if (children != null) {
+ for (int j = 0; j < children.length; j++) {
+ initializeComponent(children[j]);
+ }
+ }
+ }
+
+ /**
+ * Listens for changes in font on components rooted at this PSwing.
+ *
+ * @param evt property change event representing the change in font
+ */
+ public void propertyChange(final PropertyChangeEvent evt) {
+ final Component source = (Component) evt.getSource();
+ if (source.getFont() != null && component.isAncestorOf(source)) {
+ minFontSize = Math.min(minFontSize, source.getFont().getSize());
+ }
+ }
+
+ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ initializeComponent(component);
+ }
+
+ /**
+ * Attaches a listener to the specified node and all its parents to listen
+ * for a change in the PSwingCanvas. Only PROPERTY_PARENT listeners are
+ * added so this code wouldn't handle if a PLayer were viewed by a different
+ * PCamera since that constitutes a child change.
+ *
+ * @param node The child node at which to begin a parent-based traversal for
+ * adding listeners.
+ */
+ private void listenForCanvas(final PNode node) {
+ // need to get the full tree for this node
+ PNode p = node;
+ while (p != null) {
+ listenToNode(p);
+
+ final PNode parent = p;
+ // System.out.println( "parent = " + parent.getClass() );
+ if (parent instanceof PLayer) {
+ final PLayer player = (PLayer) parent;
+ // System.out.println( "Found player: with " +
+ // player.getCameraCount() + " cameras" );
+ for (int i = 0; i < player.getCameraCount(); i++) {
+ final PCamera cam = player.getCamera(i);
+ if (cam.getComponent() instanceof PSwingCanvas) {
+ updateCanvas((PSwingCanvas) cam.getComponent());
+ break;
+ }
+ }
+ }
+ p = p.getParent();
+ }
+ }
+
+ /**
+ * Attach a property change listener to the specified node, if one has not
+ * already been attached.
+ *
+ * @param node the node to listen to for parent/pcamera/pcanvas changes
+ */
+ private void listenToNode(final PNode node) {
+ if (!isListeningTo(node)) {
+ listeningTo.add(node);
+ node.addPropertyChangeListener(PNode.PROPERTY_PARENT, parentListener);
+ }
+ }
+
+ /**
+ * Determine whether this PSwing is already listening to the specified node
+ * for camera/canvas changes.
+ *
+ * @param node the node to check
+ * @return true if this PSwing is already listening to the specified node
+ * for camera/canvas changes
+ */
+ private boolean isListeningTo(final PNode node) {
+ for (int i = 0; i < listeningTo.size(); i++) {
+ final PNode pNode = (PNode) listeningTo.get(i);
+ if (pNode == node) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes this PSwing from previous PSwingCanvas (if any), and ensure that
+ * this PSwing is attached to the new PSwingCanvas.
+ *
+ * @param newCanvas the new PSwingCanvas (may be null)
+ */
+ private void updateCanvas(final PSwingCanvas newCanvas) {
+ if (newCanvas == canvas) {
+ return;
+ }
+
+ if (canvas != null) {
+ canvas.removePSwing(this);
+ }
+
+ if (newCanvas == null) {
+ canvas = null;
+ }
+ else {
+ canvas = newCanvas;
+ canvas.addPSwing(this);
+ updateBounds();
+ repaint();
+ canvas.invalidate();
+ canvas.revalidate();
+ canvas.repaint();
+ }
+
+ }
+
+ /**
+ * Return the Greek threshold scale. When the scale will be below this
+ * threshold the Swing component is rendered as 'Greek' instead of painting
+ * the Swing component. Defaults to {@link #DEFAULT_GREEK_THRESHOLD}.
+ *
+ * @see PSwing#paintAsGreek(Graphics2D)
+ * @return the current Greek threshold scale
+ */
+ public double getGreekThreshold() {
+ return greekThreshold;
+ }
+
+ /**
+ * Set the Greek threshold in scale to greekThreshold
. When the
+ * scale will be below this threshold the Swing component is rendered as
+ * 'Greek' instead of painting the Swing component..
+ *
+ * @see PSwing#paintAsGreek(Graphics2D)
+ * @param greekThreshold Greek threshold in scale
+ */
+ public void setGreekThreshold(final double greekThreshold) {
+ this.greekThreshold = greekThreshold;
+ invalidatePaint();
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PSwingCanvas.java b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingCanvas.java
new file mode 100644
index 0000000..a22b5c8
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingCanvas.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.pswing;
+
+import java.awt.Dimension;
+
+import javax.swing.JComponent;
+import javax.swing.RepaintManager;
+
+import edu.umd.cs.piccolo.PCanvas;
+
+/**
+ * The PSwingCanvas is a PCanvas that can display Swing components with
+ * the PSwing adapter.
+ *
+ * @author Benjamin B. Bederson
+ * @author Sam R. Reid
+ * @author Lance E. Good
+ */
+public class PSwingCanvas extends PCanvas {
+ private static final long serialVersionUID = 1L;
+ /** Key used to store the "Swing Wrapper" as an attribute of the PSwing node. */
+ public static final String SWING_WRAPPER_KEY = "Swing Wrapper";
+ private final ChildWrapper swingWrapper;
+
+ /**
+ * Construct a new PSwingCanvas.
+ */
+ public PSwingCanvas() {
+ swingWrapper = new ChildWrapper();
+ add(swingWrapper);
+ initRepaintManager();
+ new PSwingEventHandler(this, getCamera()).setActive(true);
+ }
+
+ private void initRepaintManager() {
+ final RepaintManager repaintManager = RepaintManager.currentManager(this);
+ if (!(repaintManager instanceof PSwingRepaintManager)) {
+ RepaintManager.setCurrentManager(new PSwingRepaintManager());
+ }
+ }
+
+ JComponent getSwingWrapper() {
+ return swingWrapper;
+ }
+
+ void addPSwing(final PSwing pSwing) {
+ swingWrapper.add(pSwing.getComponent());
+ }
+
+ void removePSwing(final PSwing pSwing) {
+ swingWrapper.remove(pSwing.getComponent());
+ }
+
+ /**
+ * JComponent wrapper for a PSwingCanvas. Used by PSwingRepaintManager.
+ */
+ static class ChildWrapper extends JComponent {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new JComponent wrapper for the specified PSwingCanvas.
+ */
+ public ChildWrapper() {
+ setSize(new Dimension(0, 0));
+ setPreferredSize(new Dimension(0, 0));
+ putClientProperty(SWING_WRAPPER_KEY, SWING_WRAPPER_KEY);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PSwingEvent.java b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingEvent.java
new file mode 100644
index 0000000..176cf85
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingEvent.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.pswing;
+
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.util.PPickPath;
+
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+
+/**
+ * Interface allowing PSwing events that originated from swing and are destined
+ * for PSwing nodes must conform to.
+ */
+public interface PSwingEvent {
+ /**
+ * Returns the x,y position of the event in the local coordinate system of
+ * the node the event occurred on.
+ *
+ * @return a Point2D object containing the x and y coordinates local to the
+ * node.
+ */
+ Point2D getLocalPoint();
+
+ /**
+ * Returns the horizontal x position of the event in the local coordinate
+ * system of the node the event occurred on.
+ *
+ * @return x a double indicating horizontal position local to the node.
+ */
+ double getLocalX();
+
+ /**
+ * Returns the vertical y position of the event in the local coordinate
+ * system of the node the event occurred on.
+ *
+ * @return y a double indicating vertical position local to the node.
+ */
+ double getLocalY();
+
+ /**
+ * Determine the event type.
+ *
+ * @return the id
+ */
+ int getID();
+
+ /**
+ * Determine the node the event originated at. If an event percolates up the
+ * tree and is handled by an event listener higher up in the tree than the
+ * original node that generated the event, this returns the original node.
+ * For mouse drag and release events, this is the node that the original
+ * matching press event went to - in other words, the event is 'grabbed' by
+ * the originating node.
+ *
+ * @return the node
+ */
+ PNode getNode();
+
+ /**
+ * Determine the path the event took from the PCanvas down to the visual
+ * component.
+ *
+ * @return the path
+ */
+ PPickPath getPath();
+
+ /**
+ * Determine the node the event originated at. If an event percolates up the
+ * tree and is handled by an event listener higher up in the tree than the
+ * original node that generated the event, this returns the original node.
+ * For mouse drag and release events, this is the node that the original
+ * matching press event went to - in other words, the event is 'grabbed' by
+ * the originating node.
+ *
+ * @return the node
+ */
+ PNode getGrabNode();
+
+ /**
+ * Return the path from the PCanvas down to the currently grabbed object.
+ *
+ * @return the path
+ */
+ PPickPath getGrabPath();
+
+ /**
+ * Get the current node that is under the cursor. This may return a
+ * different result then getGrabNode() when in a MOUSE_RELEASED or
+ * MOUSE_DRAGGED event.
+ *
+ * @return the current node.
+ */
+ PNode getCurrentNode();
+
+ /**
+ * Get the path from the PCanvas down to the visual component currently
+ * under the mouse.This may give a different result then getGrabPath()
+ * during a MOUSE_DRAGGED or MOUSE_RELEASED operation.
+ *
+ * @return the current path.
+ */
+ PPickPath getCurrentPath();
+
+ /**
+ * Calls appropriate method on the listener based on this events ID.
+ *
+ * @param listener the MouseListener or MouseMotionListener to dispatch to.
+ */
+ void dispatchTo(Object listener);
+
+ /**
+ * Set the source of this event. As the event is fired up the tree the
+ * source of the event will keep changing to reflect the scenegraph object
+ * that is firing the event.
+ *
+ * @param aSource the source of the event
+ */
+ void setSource(Object aSource);
+
+ /**
+ * Returns this event as a mouse event. This reduces the need to cast
+ * instances of this interface when they are known to all extend MouseEvent.
+ *
+ * @return this object casted to a MouseEvent
+ */
+ MouseEvent asMouseEvent();
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PSwingEventHandler.java b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingEventHandler.java
new file mode 100644
index 0000000..8a8f1d7
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingEventHandler.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.pswing;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Point;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseWheelEvent;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Point2D;
+
+import javax.swing.SwingUtilities;
+
+import edu.umd.cs.piccolo.PCamera;
+import edu.umd.cs.piccolo.PLayer;
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.event.PInputEvent;
+import edu.umd.cs.piccolo.event.PInputEventListener;
+import edu.umd.cs.piccolo.util.PAffineTransform;
+import edu.umd.cs.piccolo.util.PAffineTransformException;
+
+/**
+ * Event handler to send MousePressed, MouseReleased, MouseMoved, MouseClicked,
+ * and MouseDragged events on Swing components within a PCanvas.
+ *
+ * @author Ben Bederson
+ * @author Lance Good
+ * @author Sam Reid
+ */
+public class PSwingEventHandler implements PInputEventListener {
+ /** Used to listen for events. */
+ private PNode listenNode = null;
+
+ /** Tracks whether this event handler is active. */
+ private boolean active = false;
+
+ /**
+ * The previous component - used to generate mouseEntered and mouseExited
+ * events.
+ */
+ private Component previousComponent = null;
+
+ /** Previous point used for mouseEntered and exited events. */
+ private Point2D prevPoint = null;
+
+ /** Previous offset used for mouseEntered and exited events. */
+ private Point2D previousOffset = null;
+
+ /** Used to avoid accidental recursive handling. */
+ private boolean recursing = false;
+
+ /** Used for tracking the left button's state. */
+ private final ButtonData leftButtonData = new ButtonData();
+
+ /** Used for tracking the middle button's state. */
+ private final ButtonData middleButtonData = new ButtonData();
+
+ /** Used for tracking the right button's state. */
+ private final ButtonData rightButtonData = new ButtonData();
+
+ /** The Canvas in which all this pswing activity is taking place. */
+ private final PSwingCanvas canvas;
+
+ /**
+ * Constructs a new PSwingEventHandler for the given canvas, and a node that
+ * will receive the mouse events.
+ *
+ * @param canvas the canvas associated with this PSwingEventHandler.
+ * @param listenNode the node the mouse listeners will be attached to.
+ */
+ public PSwingEventHandler(final PSwingCanvas canvas, final PNode listenNode) {
+ this.canvas = canvas;
+ this.listenNode = listenNode;
+ }
+
+ /**
+ * Constructs a new PSwingEventHandler for the given canvas.
+ *
+ * @param canvas to associate this event handler to
+ */
+ public PSwingEventHandler(final PSwingCanvas canvas) {
+ this.canvas = canvas;
+ }
+
+ /**
+ * Sets whether this event handler can fire events.
+ *
+ * @param active true if this event handler can fire events
+ */
+ void setActive(final boolean active) {
+ if (this.active && !active) {
+ if (listenNode != null) {
+ this.active = false;
+ listenNode.removeInputEventListener(this);
+ }
+ }
+ else if (!this.active && active && listenNode != null) {
+ this.active = true;
+ listenNode.addInputEventListener(this);
+ }
+ }
+
+ /**
+ * Returns if this event handler is active.
+ *
+ * @return true if can fire events
+ */
+ public boolean isActive() {
+ return active;
+ }
+
+ /**
+ * Finds the best visible component or subcomponent at the specified
+ * location.
+ *
+ * @param component component to test children or self for
+ * @param x x component of location
+ * @param y y component of location
+ * @return the component or subcomponent at the specified location.
+ */
+ private Component findShowingComponentAt(final Component component, final int x, final int y) {
+ if (!component.contains(x, y)) {
+ return null;
+ }
+
+ if (component instanceof Container) {
+ final Container contain = (Container) component;
+ final Component child = findShowingChildAt(contain, x, y);
+ if (child != null) {
+ return child;
+ }
+ }
+ return component;
+ }
+
+ private Component findShowingChildAt(final Container container, final int x, final int y) {
+ final Component[] children = container.getComponents();
+
+ for (int i = 0; i < children.length; i++) {
+ Component child = children[i];
+ if (child != null) {
+ final Point p = child.getLocation();
+ if (child instanceof Container) {
+ child = findShowingComponentAt(child, x - p.x, y - p.y);
+ }
+ else {
+ child = child.getComponentAt(x - p.x, y - p.y);
+ }
+ if (child != null && child.isShowing()) {
+ return child;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Determines if any Swing components in Piccolo2D should receive the given
+ * MouseEvent and forwards the event to that component. However,
+ * mouseEntered and mouseExited are independent of the buttons. Also, notice
+ * the notes on mouseEntered and mouseExited.
+ *
+ * @param pSwingMouseEvent event being dispatched
+ * @param aEvent Piccolo2D event translation of the pSwingMouseEvent
+ */
+ void dispatchEvent(final PSwingEvent pSwingMouseEvent, final PInputEvent aEvent) {
+ final MouseEvent mEvent = pSwingMouseEvent.asMouseEvent();
+ final PNode pickedNode = pSwingMouseEvent.getPath().getPickedNode();
+ final PNode currentNode = pSwingMouseEvent.getCurrentNode();
+
+ Component comp = null;
+ Point point = null;
+
+ Point offset = new Point();
+
+ if (currentNode instanceof PSwing && pickedNode.isDescendentOf(canvas.getRoot())) {
+
+ final PSwing swing = (PSwing) currentNode;
+ final PNode grabNode = pickedNode;
+
+ point = new Point(mEvent.getX(), mEvent.getY());
+ cameraToLocal(pSwingMouseEvent.getPath().getTopCamera(), point, grabNode);
+ prevPoint = (Point) point.clone();
+
+ // This is only partially fixed to find the deepest
+ // component at pt. It needs to do something like
+ // package private method:
+ // Container.getMouseEventTarget(int,int,boolean)
+ comp = findShowingComponentAt(swing.getComponent(), point.x, point.y);
+
+ // We found the right component - but we need to
+ // get the offset to put the event in the component's
+ // coordinates
+ if (comp != null && comp != swing.getComponent()) {
+ offset = extractSwingOffset(comp, swing);
+ }
+
+ // Mouse Pressed gives focus - effects Mouse Drags and
+ // Mouse Releases
+ if (comp != null && isMousePress(pSwingMouseEvent)) {
+ if (SwingUtilities.isLeftMouseButton(mEvent)) {
+ leftButtonData.setState(pickedNode, comp, offset.x, offset.y);
+ }
+ else if (SwingUtilities.isMiddleMouseButton(mEvent)) {
+ middleButtonData.setState(pickedNode, comp, offset.x, offset.y);
+ }
+ else if (SwingUtilities.isRightMouseButton(mEvent)) {
+ rightButtonData.setState(pickedNode, comp, offset.x, offset.y);
+ }
+ }
+ }
+
+ // This first case we don't want to give events to just
+ // any Swing component - but to the one that got the
+ // original mousePressed
+ if (isDragOrRelease(pSwingMouseEvent)) {
+ if (isLeftMouseButtonOnComponent(mEvent)) {
+ handleButton(pSwingMouseEvent, aEvent, leftButtonData);
+ }
+
+ if (isMiddleMouseButtonOnComponent(mEvent)) {
+ handleButton(pSwingMouseEvent, aEvent, middleButtonData);
+ }
+
+ if (isRightMouseButtonOnComponent(mEvent)) {
+ handleButton(pSwingMouseEvent, aEvent, rightButtonData);
+ }
+ }
+ else if (isPressOrClickOrMove(pSwingMouseEvent) && comp != null) {
+ final MouseEvent tempEvent = new MouseEvent(comp, pSwingMouseEvent.getID(), mEvent.getWhen(), mEvent
+ .getModifiers(), point.x - offset.x, point.y - offset.y, mEvent.getClickCount(), mEvent
+ .isPopupTrigger());
+
+ final PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
+ dispatchEvent(comp, e2);
+ }
+ else if (isWheelEvent(pSwingMouseEvent) && comp != null) {
+ final MouseWheelEvent mWEvent = (MouseWheelEvent) mEvent;
+
+ final MouseWheelEvent tempEvent = new MouseWheelEvent(comp, pSwingMouseEvent.getID(), mEvent.getWhen(),
+ mEvent.getModifiers(), point.x - offset.x, point.y - offset.y, mEvent.getClickCount(), mEvent
+ .isPopupTrigger(), mWEvent.getScrollType(), mWEvent.getScrollAmount(), mWEvent
+ .getWheelRotation());
+
+ final PSwingMouseWheelEvent e2 = new PSwingMouseWheelEvent(tempEvent.getID(), tempEvent, aEvent);
+ dispatchEvent(comp, e2);
+ }
+
+ // Now we need to check if an exit or enter event needs to
+ // be dispatched - this code is independent of the mouseButtons.
+ // I tested in normal Swing to see the correct behavior.
+ if (previousComponent != null) {
+ // This means mouseExited
+
+ // This shouldn't happen - since we're only getting node events
+ if (comp == null || pSwingMouseEvent.getID() == MouseEvent.MOUSE_EXITED) {
+ final MouseEvent tempEvent = createExitEvent(mEvent);
+
+ final PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
+
+ dispatchEvent(previousComponent, e2);
+ previousComponent = null;
+ }
+
+ // This means mouseExited prevComponent and mouseEntered comp
+ else if (previousComponent != comp) {
+ MouseEvent tempEvent = createExitEvent(mEvent);
+ PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
+ dispatchEvent(previousComponent, e2);
+
+ tempEvent = createEnterEvent(comp, mEvent, offset.x, offset.y);
+ e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
+ comp.dispatchEvent(e2.asMouseEvent());
+ }
+ }
+ else if (comp != null) { // This means mouseEntered
+ final MouseEvent tempEvent = createEnterEvent(comp, mEvent, offset.x, offset.y);
+ final PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
+ dispatchEvent(comp, e2);
+ }
+
+ previousComponent = comp;
+
+ if (comp != null) {
+ previousOffset = offset;
+ }
+ }
+
+ private Point extractSwingOffset(final Component comp, final PSwing swing) {
+ int offsetX = 0;
+ int offsetY = 0;
+
+ for (Component c = comp; c != swing.getComponent(); c = c.getParent()) {
+ offsetX += c.getLocation().x;
+ offsetY += c.getLocation().y;
+ }
+
+ return new Point(offsetX, offsetY);
+ }
+
+ private boolean isRightMouseButtonOnComponent(final MouseEvent mEvent) {
+ return SwingUtilities.isRightMouseButton(mEvent) && rightButtonData.getFocusedComponent() != null;
+ }
+
+ private boolean isMiddleMouseButtonOnComponent(final MouseEvent mEvent) {
+ return SwingUtilities.isMiddleMouseButton(mEvent) && middleButtonData.getFocusedComponent() != null;
+ }
+
+ private boolean isLeftMouseButtonOnComponent(final MouseEvent mEvent) {
+ return SwingUtilities.isLeftMouseButton(mEvent) && leftButtonData.getFocusedComponent() != null;
+ }
+
+ private boolean isMousePress(final PSwingEvent pSwingMouseEvent) {
+ return pSwingMouseEvent.getID() == MouseEvent.MOUSE_PRESSED;
+ }
+
+ private boolean isWheelEvent(final PSwingEvent pSwingMouseEvent) {
+ return pSwingMouseEvent.getID() == MouseEvent.MOUSE_WHEEL;
+ }
+
+ private boolean isPressOrClickOrMove(final PSwingEvent pSwingMouseEvent) {
+ return isMousePress(pSwingMouseEvent) || pSwingMouseEvent.getID() == MouseEvent.MOUSE_CLICKED
+ || pSwingMouseEvent.getID() == MouseEvent.MOUSE_MOVED;
+ }
+
+ private boolean isDragOrRelease(final PSwingEvent pSwingMouseEvent) {
+ return pSwingMouseEvent.getID() == MouseEvent.MOUSE_DRAGGED
+ || pSwingMouseEvent.getID() == MouseEvent.MOUSE_RELEASED;
+ }
+
+ private MouseEvent createEnterEvent(final Component comp, final MouseEvent e1, final int offX, final int offY) {
+ return new MouseEvent(comp, MouseEvent.MOUSE_ENTERED, e1.getWhen(), 0, (int) prevPoint.getX() - offX,
+ (int) prevPoint.getY() - offY, e1.getClickCount(), e1.isPopupTrigger());
+ }
+
+ private MouseEvent createExitEvent(final MouseEvent e1) {
+ return new MouseEvent(previousComponent, MouseEvent.MOUSE_EXITED, e1.getWhen(), 0, (int) prevPoint.getX()
+ - (int) previousOffset.getX(), (int) prevPoint.getY() - (int) previousOffset.getY(),
+ e1.getClickCount(), e1.isPopupTrigger());
+ }
+
+ private void handleButton(final PSwingEvent e1, final PInputEvent aEvent, final ButtonData buttonData) {
+ final MouseEvent m1 = e1.asMouseEvent();
+ if (involvesSceneNode(buttonData)) {
+ // TODO: this probably won't handle viewing through multiple
+ // cameras.
+
+ final Point2D pt = new Point2D.Double(m1.getX(), m1.getY());
+ cameraToLocal(e1.getPath().getTopCamera(), pt, buttonData.getPNode());
+ final MouseEvent tempEvent = new MouseEvent(buttonData.getFocusedComponent(), m1.getID(), m1.getWhen(), m1
+ .getModifiers(), (int) pt.getX() - buttonData.getOffsetX(), (int) pt.getY()
+ - buttonData.getOffsetY(), m1.getClickCount(), m1.isPopupTrigger());
+
+ final PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
+ dispatchEvent(buttonData.getFocusedComponent(), e2);
+ }
+ else {
+ dispatchEvent(buttonData.getFocusedComponent(), e1);
+ }
+ // buttonData.getPSwing().repaint(); //Experiment with SliderExample
+ // (from Martin) suggests this line is unnecessary, and a serious
+ // problem in performance.
+ m1.consume();
+ if (e1.getID() == MouseEvent.MOUSE_RELEASED) {
+ buttonData.mouseReleased();
+ }
+ }
+
+ private boolean involvesSceneNode(final ButtonData buttonData) {
+ return buttonData.getPNode().isDescendentOf(canvas.getRoot());
+ }
+
+ private void dispatchEvent(final Component target, final PSwingEvent event) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ target.dispatchEvent(event.asMouseEvent());
+ }
+ });
+ }
+
+ /**
+ * Transforms the given point from camera coordinates to the node's local
+ * system.
+ *
+ * @param camera camera from which coordinates are measured
+ * @param pt point to transform (will be modified)
+ * @param node node from which local coordinates are measured
+ */
+ private void cameraToLocal(final PCamera camera, final Point2D pt, final PNode node) {
+ if (node != null) {
+ if (descendsFromLayer(node)) {
+ final AffineTransform inverse = invertTransform(camera.getViewTransform());
+ inverse.transform(pt, pt);
+ }
+
+ node.globalToLocal(pt);
+ }
+ }
+
+ /**
+ * Returns true if the provided layer has a PLayer ancestor.
+ *
+ * @param node node being tested
+ *
+ * @return true if node is a descendant of a PLayer
+ */
+ private boolean descendsFromLayer(final PNode node) {
+ PNode searchNode = node;
+ while (searchNode != null) {
+ searchNode = searchNode.getParent();
+ if (searchNode instanceof PLayer) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the inverse transform for the provided transform. Throws
+ * exception if transform is non invertible.
+ *
+ * @param transform transform to invert
+ * @return inverted transform
+ */
+ private AffineTransform invertTransform(final PAffineTransform transform) {
+ try {
+ return transform.createInverse();
+ }
+ catch (final NoninvertibleTransformException e) {
+ throw new PAffineTransformException(e, transform);
+ }
+ }
+
+ /**
+ * Process a Piccolo2D event and (if active) dispatch the corresponding
+ * Swing event.
+ *
+ * @param aEvent Piccolo2D event being tested for dispatch to swing
+ * @param type is not used in this method
+ */
+ public void processEvent(final PInputEvent aEvent, final int type) {
+ if (!aEvent.isMouseEvent()) {
+ return;
+ }
+
+ final InputEvent sourceSwingEvent = aEvent.getSourceSwingEvent();
+ if (!(sourceSwingEvent instanceof MouseEvent)) {
+ throw new RuntimeException("PInputEvent.getSourceSwingEvent was not a MouseEvent. Actual event: "
+ + sourceSwingEvent + ", class=" + sourceSwingEvent.getClass().getName());
+ }
+
+ processMouseEvent(aEvent, (MouseEvent) sourceSwingEvent);
+ }
+
+ private void processMouseEvent(final PInputEvent aEvent, final MouseEvent swingMouseEvent) {
+ if (!recursing) {
+ recursing = true;
+ final PSwingEvent pSwingMouseEvent = PSwingMouseEvent.createMouseEvent(swingMouseEvent.getID(),
+ swingMouseEvent, aEvent);
+
+ dispatchEvent(pSwingMouseEvent, aEvent);
+ if (pSwingMouseEvent.asMouseEvent().isConsumed()) {
+ aEvent.setHandled(true);
+ }
+ recursing = false;
+ }
+ }
+
+ /**
+ * Internal Utility class for handling button interactivity.
+ */
+ private static class ButtonData {
+ private PNode focusNode = null;
+ private Component focusComponent = null;
+ private int focusOffX = 0;
+ private int focusOffY = 0;
+
+ public void setState(final PNode visualNode, final Component comp, final int offX, final int offY) {
+ focusComponent = comp;
+ focusNode = visualNode;
+ focusOffX = offX;
+ focusOffY = offY;
+ }
+
+ public Component getFocusedComponent() {
+ return focusComponent;
+ }
+
+ public PNode getPNode() {
+ return focusNode;
+ }
+
+ public int getOffsetX() {
+ return focusOffX;
+ }
+
+ public int getOffsetY() {
+ return focusOffY;
+ }
+
+ public void mouseReleased() {
+ focusComponent = null;
+ focusNode = null;
+ }
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseEvent.java b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseEvent.java
new file mode 100644
index 0000000..c6407f2
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseEvent.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.pswing;
+
+import java.awt.Component;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.geom.Point2D;
+import java.io.Serializable;
+
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.event.PInputEvent;
+import edu.umd.cs.piccolo.util.PPickPath;
+
+/**
+ * PMouseEvent is an event which indicates that a mouse action occurred
+ * in a node.
+ * + * This low-level event is generated by a node object for: + *
+ * A PMouseEvent object is passed to every PMouseListener
or
+ * PMouseAdapter
object which registered to receive the
+ * "interesting" mouse events using the component's
+ * addMouseListener
method. (PMouseAdapter
objects
+ * implement the PMouseListener
interface.) Each such listener
+ * object gets a PMouseEvent
containing the mouse event.
+ *
+ * Warning: Serialized objects of this class will not be compatible with + * future Piccolo2d releases. The current serialization support is appropriate + * for short term storage or RMI between applications running the same version + * of Piccolo2d. A future release of Piccolo2d will provide support for long + * term persistence. + *
+ * + * @author Benjamin B. Bederson + * @author Sam R. Reid + * @author Lance E. Good + */ +public class PSwingMouseEvent extends MouseEvent implements Serializable, PSwingEvent { + private static final long serialVersionUID = 1L; + private final int id; + private final transient PInputEvent event; + + /** + * Constructs a new PMouse event from a Java MouseEvent. + * + * @param id The event type (MOUSE_PRESSED, MOUSE_RELEASED, MOUSE_CLICKED, + * MOUSE_ENTERED, MOUSE_EXITED) + * @param swingEvent The original swing mouse event when in MOUSE_RELEASED + * events. + * @param piccoloEvent used to query about the event's Piccolo context + */ + protected PSwingMouseEvent(final int id, final MouseEvent swingEvent, final PInputEvent piccoloEvent) { + super((Component) swingEvent.getSource(), swingEvent.getID(), swingEvent.getWhen(), swingEvent.getModifiers(), + swingEvent.getX(), swingEvent.getY(), swingEvent.getClickCount(), swingEvent.isPopupTrigger()); + this.id = id; + this.event = piccoloEvent; + } + + /** + * Creates and returns a new PMouse event from a Java MouseEvent. + * + * @param id The event type (MOUSE_PRESSED, MOUSE_RELEASED, MOUSE_CLICKED, + * MOUSE_ENTERED, MOUSE_EXITED, MOUSE_MOVED, MOUSE_DRAGGED) + * @param swingEvent The original swing mouse event when in + * MOUSE_DRAGGED and MOUSE_RELEASED events. + * @param pEvent used to query about the event's Piccolo2d context + * + * @return the constructed PSwingEvent + */ + public static PSwingEvent createMouseEvent(final int id, final MouseEvent swingEvent, final PInputEvent pEvent) { + if (pEvent == null) { + throw new IllegalArgumentException("PInputEvent associated with PSwingEvent may not be null"); + } + + if (id == MouseEvent.MOUSE_MOVED || id == MouseEvent.MOUSE_DRAGGED) { + return new PSwingMouseMotionEvent(id, swingEvent, pEvent); + } + else if (id == MouseEvent.MOUSE_WHEEL) { + return new PSwingMouseWheelEvent(id, (MouseWheelEvent) swingEvent, pEvent); + } + else { + return new PSwingMouseEvent(id, swingEvent, pEvent); + } + } + + /** + * Returns the x,y position of the event in the local coordinate system of + * the node the event occurred on. + * + * @return a Point2D object containing the x and y coordinates local to the + * node. + */ + public Point2D getLocalPoint() { + return new Point2D.Double(getX(), getY()); + } + + /** + * Returns the horizontal x position of the event in the local coordinate + * system of the node the event occurred on. + * + * @return x a double indicating horizontal position local to the node. + */ + public double getLocalX() { + return getLocalPoint().getX(); + } + + /** + * Returns the vertical y position of the event in the local coordinate + * system of the node the event occurred on. + * + * @return y a double indicating vertical position local to the node. + */ + public double getLocalY() { + return getLocalPoint().getY(); + } + + /** + * Determine the event type. + * + * @return the id + */ + public int getID() { + return id; + } + + /** + * Determine the node the event originated at. If an event percolates up the + * tree and is handled by an event listener higher up in the tree than the + * original node that generated the event, this returns the original node. + * For mouse drag and release events, this is the node that the original + * matching press event went to - in other words, the event is 'grabbed' by + * the originating node. + * + * @return the node + */ + public PNode getNode() { + return event.getPickedNode(); + } + + /** + * Determine the path the event took from the PCanvas down to the visual + * component. + * + * @return the path + */ + public PPickPath getPath() { + return event.getPath(); + } + + /** + * Determine the node the event originated at. If an event percolates up the + * tree and is handled by an event listener higher up in the tree than the + * original node that generated the event, this returns the original node. + * For mouse drag and release events, this is the node that the original + * matching press event went to - in other words, the event is 'grabbed' by + * the originating node. + * + * @return the node + */ + public PNode getGrabNode() { + return event.getPickedNode(); + } + + /** + * Return the path from the PCanvas down to the currently grabbed object. + * + * @return the path + */ + public PPickPath getGrabPath() { + return getPath(); + } + + /** + * Get the current node that is under the cursor. This may return a + * different result then getGrabNode() when in a MOUSE_RELEASED or + * MOUSE_DRAGGED event. + * + * @return the current node. + */ + public PNode getCurrentNode() { + return event.getPickedNode(); + } + + /** + * Get the path from the PCanvas down to the visual component currently + * under the mouse.This may give a different result then getGrabPath() + * durring a MOUSE_DRAGGED or MOUSE_RELEASED operation. + * + * @return the current path. + */ + public PPickPath getCurrentPath() { + return getPath(); + } + + /** + * Calls appropriate method on the listener based on this events ID. + * + * @param listener the MouseListener or MouseMotionListener to dispatch to. + */ + public void dispatchTo(final Object listener) { + final MouseListener mouseListener = (MouseListener) listener; + switch (getID()) { + case MouseEvent.MOUSE_CLICKED: + mouseListener.mouseClicked(this); + break; + case MouseEvent.MOUSE_ENTERED: + mouseListener.mouseEntered(this); + break; + case MouseEvent.MOUSE_EXITED: + mouseListener.mouseExited(this); + break; + case MouseEvent.MOUSE_PRESSED: + mouseListener.mousePressed(this); + break; + case MouseEvent.MOUSE_RELEASED: + mouseListener.mouseReleased(this); + break; + default: + throw new RuntimeException("PMouseEvent with bad ID"); + } + } + + /** + * Set the souce of this event. As the event is fired up the tree the source + * of the event will keep changing to reflect the scenegraph object that is + * firing the event. + * + * @param newSource the currently reported source of the event (will change + * as event is bubbled up) + */ + public void setSource(final Object newSource) { + source = newSource; + } + + /** + * Returns this PSwingMouseEvent's MouseEvent. + * + * @return underlying mouse event of this PSwingMouseEvent + */ + public MouseEvent asMouseEvent() { + return this; + } +} \ No newline at end of file diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseMotionEvent.java b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseMotionEvent.java new file mode 100644 index 0000000..1ab95aa --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseMotionEvent.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.pswing; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; + +import edu.umd.cs.piccolo.event.PInputEvent; + +/** + * PMouseMotionEvent is an event which indicates that a mouse motion + * action occurred in a node. + *+ * This low-level event is generated by a node object for: + *
+ * A PMouseEvent object is passed to every PMouseMotionListener
or
+ * PMouseMotionAdapter
object which registered to receive mouse
+ * motion events using the component's addMouseMotionListener
+ * method. (PMouseMotionAdapter
objects implement the
+ * PMouseMotionListener
interface.) Each such listener object gets
+ * a PMouseEvent
containing the mouse motion event.
+ *
+ * Warning: Serialized objects of this class will not be compatible with + * future Piccolo releases. The current serialization support is appropriate for + * short term storage or RMI between applications running the same version of + * Piccolo. A future release of Piccolo will provide support for long term + * persistence. + *
+ * + * @author Benjamin B. Bederson + * @author Sam R. Reid + * @author Lance E. Good + */ +public class PSwingMouseMotionEvent extends PSwingMouseEvent { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new PMouse event from a Java MouseEvent. + * + * @param id The event type (MOUSE_MOVED, MOUSE_DRAGGED) + * @param swingEvent The original Java mouse event when in MOUSE_DRAGGED events + * @param piccoloEvent Piccolo2d event to use when querying about the event's + * piccolo2d context + */ + protected PSwingMouseMotionEvent(final int id, final MouseEvent swingEvent, final PInputEvent piccoloEvent) { + super(id, swingEvent, piccoloEvent); + } + + /** + * Calls appropriate method on the listener based on this events ID. + * + * @param listener the target for dispatch. + */ + public void dispatchTo(final Object listener) { + final MouseMotionListener mouseMotionListener = (MouseMotionListener) listener; + switch (getID()) { + case MouseEvent.MOUSE_DRAGGED: + mouseMotionListener.mouseDragged(this); + break; + case MouseEvent.MOUSE_MOVED: + mouseMotionListener.mouseMoved(this); + break; + default: + throw new RuntimeException("PMouseMotionEvent with bad ID"); + } + } + +} \ No newline at end of file diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseWheelEvent.java b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseWheelEvent.java new file mode 100644 index 0000000..9176694 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingMouseWheelEvent.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.pswing; + +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.util.PPickPath; + +import java.awt.Component; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.geom.Point2D; + +/** + * PMouseMotionEvent is an event which indicates that a mouse motion + * action occurred in a node. + * + * This low-level event is generated by a node object for: + *PMouseMotionListener
or
+ * PMouseMotionAdapter
object which registered to receive mouse
+ * motion events using the component's addMouseMotionListener
+ * method. (PMouseMotionAdapter
objects implement the
+ * PMouseMotionListener
interface.) Each such listener object gets
+ * a PMouseEvent
containing the mouse motion event.
+ *
+ *
+ * Warning: Serialized objects of this class will not be compatible with
+ * future Piccolo releases. The current serialization support is appropriate for
+ * short term storage or RMI between applications running the same version of
+ * Piccolo. A future release of Piccolo will provide support for long term
+ * persistence.
+ *
+ * @author Benjamin B. Bederson
+ * @author Sam R. Reid
+ * @author Lance E. Good
+ */
+public class PSwingMouseWheelEvent extends MouseWheelEvent implements PSwingEvent {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+ private final int id;
+ private final PInputEvent event;
+
+ /**
+ * Constructs a new PMouseWheel event from a Java MouseWheelEvent.
+ *
+ * @param id The event type (MOUSE_WHEEL)
+ * @param swingEvent The original swing mouse wheel event.
+ * @param piccoloEvent Piccolo2D event for use when querying about the
+ * event's piccolo2d context
+ */
+ protected PSwingMouseWheelEvent(final int id, final MouseWheelEvent swingEvent, final PInputEvent piccoloEvent) {
+ super((Component) swingEvent.getSource(), swingEvent.getID(), swingEvent.getWhen(), swingEvent.getModifiers(),
+ swingEvent.getX(), swingEvent.getY(), swingEvent.getClickCount(), swingEvent.isPopupTrigger(),
+ swingEvent.getScrollType(), swingEvent.getScrollAmount(), swingEvent.getWheelRotation());
+ this.id = id;
+ this.event = piccoloEvent;
+ }
+
+ /**
+ * Returns the x,y position of the event in the local coordinate system of
+ * the node the event occurred on.
+ *
+ * @return a Point2D object containing the x and y coordinates local to the
+ * node.
+ */
+ public Point2D getLocalPoint() {
+ return new Point2D.Double(getX(), getY());
+ }
+
+ /**
+ * Returns the horizontal x position of the event in the local coordinate
+ * system of the node the event occurred on.
+ *
+ * @return x a double indicating horizontal position local to the node.
+ */
+ public double getLocalX() {
+ return getLocalPoint().getX();
+ }
+
+ /**
+ * Returns the vertical y position of the event in the local coordinate
+ * system of the node the event occurred on.
+ *
+ * @return y a double indicating vertical position local to the node.
+ */
+ public double getLocalY() {
+ return getLocalPoint().getY();
+ }
+
+ /**
+ * Determine the event type.
+ *
+ * @return the id
+ */
+ public int getID() {
+ return id;
+ }
+
+ /**
+ * Determine the node the event originated at. If an event percolates up the
+ * tree and is handled by an event listener higher up in the tree than the
+ * original node that generated the event, this returns the original node.
+ * For mouse drag and release events, this is the node that the original
+ * matching press event went to - in other words, the event is 'grabbed' by
+ * the originating node.
+ *
+ * @return the node
+ */
+ public PNode getNode() {
+ return event.getPickedNode();
+ }
+
+ /**
+ * Determine the path the event took from the PCanvas down to the visual
+ * component.
+ *
+ * @return the path
+ */
+ public PPickPath getPath() {
+ return event.getPath();
+ }
+
+ /**
+ * Determine the node the event originated at. If an event percolates up the
+ * tree and is handled by an event listener higher up in the tree than the
+ * original node that generated the event, this returns the original node.
+ * For mouse drag and release events, this is the node that the original
+ * matching press event went to - in other words, the event is 'grabbed' by
+ * the originating node.
+ *
+ * @return the node
+ */
+ public PNode getGrabNode() {
+ return event.getPickedNode();
+ }
+
+ /**
+ * Return the path from the PCanvas down to the currently grabbed object.
+ *
+ * @return the path
+ */
+ public PPickPath getGrabPath() {
+ return getPath();
+ }
+
+ /**
+ * Get the current node that is under the cursor. This may return a
+ * different result then getGrabNode() when in a MOUSE_RELEASED or
+ * MOUSE_DRAGGED event.
+ *
+ * @return the current node.
+ */
+ public PNode getCurrentNode() {
+ return event.getPickedNode();
+ }
+
+ /**
+ * Get the path from the PCanvas down to the visual component currently
+ * under the mouse.This may give a different result then getGrabPath()
+ * durring a MOUSE_DRAGGED or MOUSE_RELEASED operation.
+ *
+ * @return the current path.
+ */
+ public PPickPath getCurrentPath() {
+ return getPath();
+ }
+
+ /**
+ * Calls appropriate method on the listener based on this events ID.
+ *
+ * @param listener the target for dispatch.
+ */
+ public void dispatchTo(final Object listener) {
+ final MouseWheelListener mouseWheelListener = (MouseWheelListener) listener;
+ switch (getID()) {
+ case MouseEvent.MOUSE_WHEEL:
+ mouseWheelListener.mouseWheelMoved(this);
+ break;
+ default:
+ throw new RuntimeException("PMouseWheelEvent with bad ID");
+ }
+ }
+
+ /**
+ * Set the souce of this event. As the event is fired up the tree the source
+ * of the event will keep changing to reflect the scenegraph object that is
+ * firing the event.
+ *
+ * @param newSource the current source of the event to report
+ */
+ public void setSource(final Object newSource) {
+ source = newSource;
+ }
+
+ /**
+ * Returns this event as a mouse event. This reduces the need to cast
+ * instances of this interface when they are known to all extend MouseEvent.
+ *
+ * @return this object casted to a MouseEvent
+ */
+ public MouseEvent asMouseEvent() {
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/PSwingRepaintManager.java b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingRepaintManager.java
new file mode 100644
index 0000000..a98ad46
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/pswing/PSwingRepaintManager.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.pswing;
+
+import java.awt.Component;
+import java.util.Vector;
+
+import javax.swing.JComponent;
+import javax.swing.RepaintManager;
+
+import edu.umd.cs.piccolo.util.PBounds;
+
+/**
+ * This RepaintManager replaces the default Swing implementation, and is used to
+ * repaint dirty regions of PSwing components and make sure the PSwings have
+ * the appropriate size.
+ * + * This is an internal class used by Piccolo to support Swing components in + * Piccolo. This should not be instantiated, though all the public methods of + * javax.swing.RepaintManager may still be called and perform in the expected + * manner. + *
+ *+ * PBasicRepaint Manager is an extension of RepaintManager that traps those + * repaints called by the Swing components that have been added to the PCanvas + * and passes these repaints to the PSwing rather than up the + * component hierarchy as usually happens. + *
+ *+ * Also traps invalidate calls made by the Swing components added to the PCanvas + * to reshape the corresponding PSwing. + *
+ *+ * Also keeps a list of PSwings that are painting. This disables repaint until + * the component has finished painting. This is to address a problem introduced + * by Swing's CellRendererPane which is itself a work-around. The problem is + * that JTable's, JTree's, and JList's cell renderers need to be validated + * before repaint. Since we have to repaint the entire Swing component hierarchy + * (in the case of a PSwing), this causes an infinite loop. So we introduce the + * restriction that no repaints can be triggered by a call to paint. + *
+ * + * @author Benjamin B. Bederson + * @author Lance E. Good + * @author Sam R. Reid + * @author Chris Malley (cmalley@pixelzoom.com) + */ +public class PSwingRepaintManager extends RepaintManager { + + // The components that are currently painting + // This needs to be a vector for thread safety + private final Vector paintingComponents = new Vector(); + + /** + * Locks repaint for a particular (Swing) component displayed by PCanvas. + * + * @param c The component for which the repaint is to be locked + */ + public void lockRepaint(final JComponent c) { + paintingComponents.addElement(c); + } + + /** + * Unlocks repaint for a particular (Swing) component displayed by PCanvas. + * + * @param c The component for which the repaint is to be unlocked + */ + public void unlockRepaint(final JComponent c) { + paintingComponents.remove(c); + } + + /** + * Returns true if repaint is currently locked for a component and false + * otherwise. + * + * @param c The component for which the repaint status is desired + * @return Whether the component is currently painting + */ + public boolean isPainting(final JComponent c) { + return paintingComponents.contains(c); + } + + /** + * This is the method "repaint" now calls in the Swing components. + * Overridden to capture repaint calls from those Swing components which are + * being used as Piccolo visual components and to call the Piccolo repaint + * mechanism rather than the traditional Component hierarchy repaint + * mechanism. Otherwise, behaves like the superclass. + * + * @param component Component to be repainted + * @param x X coordinate of the dirty region in the component + * @param y Y coordinate of the dirty region in the component + * @param width Width of the dirty region in the component + * @param height Height of the dirty region in the component + */ + public synchronized void addDirtyRegion(final JComponent component, final int x, final int y, final int width, final int height) { + boolean captureRepaint = false; + JComponent childComponent = null; + int captureX = x; + int captureY = y; + + // We have to check to see if the PCanvas (ie. the SwingWrapper) is in the components ancestry. If so, we will + // want to capture that repaint. However, we also will need to translate the repaint request since the component + // may be offset inside another component. + for (Component comp = component; comp != null && comp.isLightweight(); comp = comp.getParent()) { + if (comp.getParent() instanceof PSwingCanvas.ChildWrapper) { + captureRepaint = true; + childComponent = (JComponent) comp; + break; + } + else { + // Adds to the offset since the component is nested + captureX += comp.getLocation().getX(); + captureY += comp.getLocation().getY(); + } + } + + // Now we check to see if we should capture the repaint and act accordingly + if (captureRepaint) { + if (!isPainting(childComponent)) { + final double repaintW = Math.min(childComponent.getWidth() - captureX, width); + final double repaintH = Math.min(childComponent.getHeight() - captureY, height); + + //Schedule a repaint for the dirty part of the PSwing + getPSwing(childComponent).repaint(new PBounds(captureX, captureY, repaintW, repaintH)); + } + } + else { + super.addDirtyRegion(component, x, y, width, height); + } + } + + /** + * This is the method "invalidate" calls in the Swing components. Overridden + * to capture invalidation calls from those Swing components being used as + * Piccolo visual components and to update Piccolo's visual component + * wrapper bounds (these are stored separately from the Swing component). + * Otherwise, behaves like the superclass. + * + * @param invalidComponent The Swing component that needs validation + */ + public synchronized void addInvalidComponent(final JComponent invalidComponent) { + if (invalidComponent.getParent() == null || !(invalidComponent.getParent() instanceof PSwingCanvas.ChildWrapper)) { + super.addInvalidComponent(invalidComponent); + } + else { + invalidComponent.validate(); + getPSwing(invalidComponent).updateBounds(); + } + } + + /** + * Obtains the PSwing associated with the specified component. + * @param component the component for which to return the associated PSwing + * @return the associated PSwing + */ + private PSwing getPSwing(JComponent component) { + return (PSwing) component.getClientProperty( PSwing.PSWING_PROPERTY ); + } +} \ No newline at end of file diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/package.html b/src/main/java/edu/umd/cs/piccolox/pswing/package.html new file mode 100644 index 0000000..f95f3bf --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/pswing/package.html @@ -0,0 +1,34 @@ + + + +This package contains PSwing nodes and related classes.
+ + diff --git a/src/main/java/edu/umd/cs/piccolox/pswing/readme.txt b/src/main/java/edu/umd/cs/piccolox/pswing/readme.txt new file mode 100644 index 0000000..3bdb2fa --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/pswing/readme.txt @@ -0,0 +1,16 @@ +This directory contains source and examples for embedding Swing components in a Piccolo hierarchy. This code was ported from a Jazz implementation. + +Example usage: + + JSlider js = new JSlider( 0, 100 ); + PSwing pSwing = new PSwing( pswingCanvas, js ); + l.addChild( pSwing ); + +Known Issues +o Handling cursors on Swing components is not yet supported. +o Creation of a PSwing currently requires an instance of the PSwingCanvas in which the component will appear. Future versions could delete this requirement, so that the constructor is simply PSwing(JComponent), and the PSwing can appear in many PSwingCanvases. + +This code has been tested in a variety of situations by 4 or 5 independent users, but with more users, some bugs will be most likely be exposed. (This code comes with NO WARRANTY, etc.) + +Sam Reid +reids@colorado.edu \ No newline at end of file diff --git a/src/main/java/edu/umd/cs/piccolox/swing/PCacheCanvas.java b/src/main/java/edu/umd/cs/piccolox/swing/PCacheCanvas.java new file mode 100644 index 0000000..ff174fa --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/swing/PCacheCanvas.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.swing; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PCanvas; +import edu.umd.cs.piccolo.PLayer; +import edu.umd.cs.piccolo.PRoot; +import edu.umd.cs.piccolox.nodes.PCacheCamera; + +/** + * An extension of PCanvas that automatically installs a PCacheCamera. + * + * @author Lance Good + */ +public class PCacheCanvas extends PCanvas { + + private static final long serialVersionUID = 1L; + + /** + * Creates a default scene with 1 root, 1 layer, and 1 PCacheCamera. + * + * @return constructed scene with PCacheCamera + */ + protected PCamera createDefaultCamera() { + final PRoot r = new PRoot(); + final PLayer l = new PLayer(); + final PCamera c = new PCacheCamera(); + + r.addChild(c); + r.addChild(l); + c.addLayer(l); + + return c; + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/swing/PDefaultScrollDirector.java b/src/main/java/edu/umd/cs/piccolox/swing/PDefaultScrollDirector.java new file mode 100644 index 0000000..7db3ae8 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/swing/PDefaultScrollDirector.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.swing; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Iterator; +import java.util.List; + +import javax.swing.ScrollPaneConstants; + +import edu.umd.cs.piccolo.PCamera; +import edu.umd.cs.piccolo.PCanvas; +import edu.umd.cs.piccolo.PLayer; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.PRoot; +import edu.umd.cs.piccolo.util.PAffineTransform; +import edu.umd.cs.piccolo.util.PBounds; + +/** + * The default scroll director implementation. This default implementation + * follows the widely accepted model of scrolling - namely the scrollbars + * control the movement of the window over the document rather than the movement + * of the document under the window. + * + * @author Lance Good + */ +public class PDefaultScrollDirector implements PScrollDirector, PropertyChangeListener { + + /** The viewport that signals this scroll director. */ + protected PViewport viewPort; + + /** The scrollpane that contains the viewport. */ + protected PScrollPane scrollPane; + + /** The canvas that this class directs. */ + protected PCanvas view; + + /** The canvas' camera. */ + protected PCamera camera; + + /** The canvas' root. */ + protected PRoot root; + + /** Flag to indicate when scrolling is currently in progress. */ + protected boolean scrollInProgress = false; + + /** + * The default constructor. + */ + public PDefaultScrollDirector() { + } + + /** + * Installs the scroll director and adds the appropriate listeners. + * + * @param targetViewPort viewport on which this director directs + * @param targetView PCanvas that the viewport looks at + */ + public void install(final PViewport targetViewPort, final PCanvas targetView) { + scrollPane = (PScrollPane) targetViewPort.getParent(); + this.viewPort = targetViewPort; + this.view = targetView; + + if (targetView != null) { + camera = targetView.getCamera(); + root = targetView.getRoot(); + } + + if (camera != null) { + camera.addPropertyChangeListener(this); + } + if (root != null) { + root.addPropertyChangeListener(this); + } + + if (scrollPane != null) { + scrollPane.revalidate(); + } + } + + /** + * Uninstall the scroll director from the viewport. + */ + public void unInstall() { + viewPort = null; + view = null; + + if (camera != null) { + camera.removePropertyChangeListener(this); + } + if (root != null) { + root.removePropertyChangeListener(this); + } + + camera = null; + root = null; + } + + /** + * Get the View position given the specified camera bounds. + * + * @param viewBounds The bounds for which the view position will be computed + * @return The view position + */ + public Point getViewPosition(final Rectangle2D viewBounds) { + final Point pos = new Point(); + if (camera != null) { + // First we compute the union of all the layers + final PBounds layerBounds = new PBounds(); + final List layers = camera.getLayersReference(); + for (final Iterator i = layers.iterator(); i.hasNext();) { + final PLayer layer = (PLayer) i.next(); + layerBounds.add(layer.getFullBoundsReference()); + } + + // Then we put the bounds into camera coordinates and + // union the camera bounds + camera.viewToLocal(layerBounds); + layerBounds.add(viewBounds); + + pos.setLocation((int) (viewBounds.getX() - layerBounds.getX() + 0.5), (int) (viewBounds.getY() + - layerBounds.getY() + 0.5)); + } + + return pos; + } + + /** + * Get the size of the view based on the specified camera bounds. + * + * @param viewBounds The view bounds for which the view size will be + * computed + * @return The view size + */ + public Dimension getViewSize(final Rectangle2D viewBounds) { + final Dimension size = new Dimension(); + if (camera != null) { + // First we compute the union of all the layers + final PBounds bounds = new PBounds(); + final List layers = camera.getLayersReference(); + for (final Iterator i = layers.iterator(); i.hasNext();) { + final PLayer layer = (PLayer) i.next(); + bounds.add(layer.getFullBoundsReference()); + } + + // Then we put the bounds into camera coordinates and + // union the camera bounds + if (!bounds.isEmpty()) { + camera.viewToLocal(bounds); + } + bounds.add(viewBounds); + + size.setSize((int) (bounds.getWidth() + 0.5), (int) (bounds.getHeight() + 0.5)); + } + + return size; + } + + /** + * Set the view position in a manner consistent with standardized scrolling. + * + * @param x The new x position + * @param y The new y position + */ + public void setViewPosition(final double x, final double y) { + // Bail out if scrollInProgress because we can end up with an infinite + // loop since the scrollbars depend on the camera location + if (camera == null || scrollInProgress) { + return; + } + + scrollInProgress = true; + + // Get the union of all the layers' bounds + final PBounds layerBounds = new PBounds(); + final List layers = camera.getLayersReference(); + for (final Iterator i = layers.iterator(); i.hasNext();) { + final PLayer layer = (PLayer) i.next(); + layerBounds.add(layer.getFullBoundsReference()); + } + + final PAffineTransform at = camera.getViewTransform(); + at.transform(layerBounds, layerBounds); + + // Union the camera bounds + final PBounds viewBounds = camera.getBoundsReference(); + layerBounds.add(viewBounds); + + // Now find the new view position in view coordinates + final Point2D newPoint = new Point2D.Double(layerBounds.getX() + x, layerBounds.getY() + y); + + // Now transform the new view position into global coords + camera.localToView(newPoint); + + // Compute the new matrix values to put the camera at the + // correct location + final double newX = -(at.getScaleX() * newPoint.getX() + at.getShearX() * newPoint.getY()); + final double newY = -(at.getShearY() * newPoint.getX() + at.getScaleY() * newPoint.getY()); + + at.setTransform(at.getScaleX(), at.getShearY(), at.getShearX(), at.getScaleY(), newX, newY); + + // Now actually set the camera's transform + camera.setViewTransform(at); + scrollInProgress = false; + } + + /** + * Invoked when the camera's view changes, or the bounds of the root or + * camera changes. + * + * @param pce property change event to examine + */ + public void propertyChange(final PropertyChangeEvent pce) { + final boolean isRelevantViewEvent = PCamera.PROPERTY_VIEW_TRANSFORM.equals(pce.getPropertyName()); + final boolean isRelevantBoundsEvent = isBoundsChangedEvent(pce) + && (pce.getSource() == camera || pce.getSource() == view.getRoot()); + + if (isRelevantViewEvent || isRelevantBoundsEvent) { + if (shouldRevalidateScrollPane()) { + scrollPane.revalidate(); + } + else { + viewPort.fireStateChanged(); + } + } + } + + private boolean isBoundsChangedEvent(final PropertyChangeEvent pce) { + return PNode.PROPERTY_BOUNDS.equals(pce.getPropertyName()) || PNode.PROPERTY_FULL_BOUNDS.equals(pce.getPropertyName()); + } + + /** + * Should the ScrollPane be revalidated. This occurs when either the scroll + * bars are showing and should be remove or are not showing and should be + * added. + * + * @return Whether the scroll pane should be revalidated + */ + public boolean shouldRevalidateScrollPane() { + if (camera != null) { + if (scrollPane.getHorizontalScrollBarPolicy() != ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED + && scrollPane.getVerticalScrollBarPolicy() != ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED) { + return false; + } + + // Get the union of all the layers' bounds + final PBounds layerBounds = new PBounds(); + final List layers = camera.getLayersReference(); + for (final Iterator i = layers.iterator(); i.hasNext();) { + final PLayer layer = (PLayer) i.next(); + layerBounds.add(layer.getFullBoundsReference()); + } + + // Put into camera coordinates + camera.viewToLocal(layerBounds); + + // And union with the camera bounds + final PBounds cameraBounds = camera.getBoundsReference(); + layerBounds.add(cameraBounds); + + // Truncate these to ints before comparing since + // that's what the ScrollPane uses + final int layerWidth = (int) (layerBounds.getWidth() + 0.5); + final int layerHeight = (int) (layerBounds.getHeight() + 0.5); + final int cameraWidth = (int) (cameraBounds.getWidth() + 0.5); + final int cameraHeight = (int) (cameraBounds.getHeight() + 0.5); + + if (scrollPane.getHorizontalScrollBar().isShowing() && layerWidth <= cameraWidth + || !scrollPane.getHorizontalScrollBar().isShowing() && layerWidth > cameraWidth + || scrollPane.getVerticalScrollBar().isShowing() && layerHeight <= cameraHeight + || !scrollPane.getVerticalScrollBar().isShowing() && layerHeight > cameraHeight) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/swing/PScrollDirector.java b/src/main/java/edu/umd/cs/piccolox/swing/PScrollDirector.java new file mode 100644 index 0000000..fc4b561 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/swing/PScrollDirector.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.swing; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.geom.Rectangle2D; + +import edu.umd.cs.piccolo.PCanvas; + +/** + * The interface an application can implement to control scrolling in a + * PScrollPane->PViewport->ZCanvas component hierarchy. + * + * @see PDefaultScrollDirector + * @author Lance Good + */ +public interface PScrollDirector { + + /** + * Installs the scroll director. + * + * @param viewport The viewport on which this director directs + * @param view The ZCanvas that the viewport looks at + */ + void install(PViewport viewport, PCanvas view); + + /** + * Uninstall the scroll director. + */ + void unInstall(); + + /** + * Get the View position given the specified camera bounds. + * + * @param viewBounds The bounds for which the view position will be computed + * @return The view position + */ + Point getViewPosition(Rectangle2D viewBounds); + + /** + * Set the view position. + * + * @param x The new x position + * @param y The new y position + */ + void setViewPosition(double x, double y); + + /** + * Get the size of the view based on the specified camera bounds. + * + * @param viewBounds The view bounds for which the view size will be + * computed + * @return The view size + */ + Dimension getViewSize(Rectangle2D viewBounds); +} diff --git a/src/main/java/edu/umd/cs/piccolox/swing/PScrollPane.java b/src/main/java/edu/umd/cs/piccolox/swing/PScrollPane.java new file mode 100644 index 0000000..bce6fc0 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/swing/PScrollPane.java @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.swing; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; + +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JViewport; +import javax.swing.Scrollable; +import javax.swing.SwingConstants; +import javax.swing.plaf.ScrollPaneUI; + +import edu.umd.cs.piccolo.PCanvas; + +/** + * A simple extension to a standard scroll pane that uses the jazz version of + * the viewport by default. Also uses the jazz version of ScrollPaneLayout + * + * @author Lance Good + */ +public class PScrollPane extends JScrollPane { + + private static final long serialVersionUID = 1L; + + /** A reusable null action. */ + protected PNullAction nullAction = null; + + /** Controls whether key actions are disabled on this component. */ + protected boolean disableKeyActions = false; + + private final AdjustmentListener scrollAdjustmentListener = new AdjustmentListener() { + private boolean lastAdjustingState = false; + + public void adjustmentValueChanged(final AdjustmentEvent event) { + if (event.getSource() instanceof JScrollBar) { + JScrollBar scrollBar = (JScrollBar) event.getSource(); + + setAdjusting(scrollBar.getValueIsAdjusting()); + } + } + + /** + * Updates the underlying PCanvas' interacting flag depending on whether + * scroll bar adjustments are still taking place. + * + * @param isAdjusting true if the scroll bar is still being interacted + * with + */ + private void setAdjusting(final boolean isAdjusting) { + if (isAdjusting != lastAdjustingState) { + Component c = getViewport().getView(); + if (c instanceof PCanvas) { + ((PCanvas) c).setInteracting(isAdjusting); + } + lastAdjustingState = isAdjusting; + } + } + }; + + /** + * Constructs a scollpane for the provided component with the specified + * scrollbar policies. + * + * @param view component being viewed through the scrollpane + * @param vsbPolicy vertical scroll bar policy + * @param hsbPolicy horizontal scroll bar policy + */ + public PScrollPane(final Component view, final int vsbPolicy, final int hsbPolicy) { + super(view, vsbPolicy, hsbPolicy); + + // Set the layout and sync it with the scroll pane + final PScrollPaneLayout layout = new PScrollPaneLayout.UIResource(); + setLayout(layout); + layout.syncWithScrollPane(this); + + horizontalScrollBar.addAdjustmentListener(scrollAdjustmentListener); + verticalScrollBar.addAdjustmentListener(scrollAdjustmentListener); + } + + /** + * Intercepts the vertical scroll bar setter to ensure that the adjustment + * listener is installed appropriately. + * + * @param newVerticalScrollBar the new vertical scroll bar to use with this PScrollPane + */ + public void setVerticalScrollBar(final JScrollBar newVerticalScrollBar) { + if (verticalScrollBar != null) { + verticalScrollBar.removeAdjustmentListener(scrollAdjustmentListener); + } + + super.setVerticalScrollBar(newVerticalScrollBar); + newVerticalScrollBar.addAdjustmentListener(scrollAdjustmentListener); + } + + /** + * Intercepts the horizontal scroll bar setter to ensure that the adjustment + * listener is installed appropriately. + * + * @param newHorizontalScrollBar the new horizontal scroll bar to use with this PScrollPane + */ + public void setHorizontalScrollBar(final JScrollBar newHorizontalScrollBar) { + if (horizontalScrollBar != null) { + horizontalScrollBar.removeAdjustmentListener(scrollAdjustmentListener); + } + + super.setHorizontalScrollBar(newHorizontalScrollBar); + newHorizontalScrollBar.addAdjustmentListener(scrollAdjustmentListener); + } + + /** + * Constructs a scroll pane for the provided component. + * + * @param view component being viewed through the scroll pane + */ + public PScrollPane(final Component view) { + this(view, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED); + } + + /** + * Constructs a scroll pane not attached to any component with the specified + * scroll bar policies. + * + * @param vsbPolicy vertical scroll bar policy + * @param hsbPolicy horizontal scroll bar policy + */ + public PScrollPane(final int vsbPolicy, final int hsbPolicy) { + this(null, vsbPolicy, hsbPolicy); + } + + /** + * Constructs a scroll pane not attached to any component. + */ + public PScrollPane() { + this(null, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED); + } + + /** + * Disable or enable key actions on this PScrollPane. + * + * @param disable true disables key actions, false enables key actions + */ + public void setKeyActionsDisabled(final boolean disable) { + if (disable && disableKeyActions != disable) { + disableKeyActions = disable; + disableKeyActions(); + } + else if (!disable && disableKeyActions != disable) { + disableKeyActions = disable; + installCustomKeyActions(); + } + } + + /** + * Sets the UI. + * + * @param ui the scroll pane UI to associate with this PScollPane + */ + public void setUI(final ScrollPaneUI ui) { + super.setUI(ui); + + if (!disableKeyActions) { + installCustomKeyActions(); + } + else { + disableKeyActions(); + } + } + + /** + * Install custom key actions (in place of the Swing defaults) to correctly + * scroll the view. + */ + protected void installCustomKeyActions() { + final ActionMap map = getActionMap(); + + map.put("scrollUp", new PScrollAction("scrollUp", SwingConstants.VERTICAL, -1, true)); + map.put("scrollDown", new PScrollAction("scrollDown", SwingConstants.VERTICAL, 1, true)); + map.put("scrollLeft", new PScrollAction("scrollLeft", SwingConstants.HORIZONTAL, -1, true)); + + map.put("scrollRight", new PScrollAction("ScrollRight", SwingConstants.HORIZONTAL, 1, true)); + map.put("unitScrollRight", new PScrollAction("UnitScrollRight", SwingConstants.HORIZONTAL, 1, false)); + map.put("unitScrollLeft", new PScrollAction("UnitScrollLeft", SwingConstants.HORIZONTAL, -1, false)); + map.put("unitScrollUp", new PScrollAction("UnitScrollUp", SwingConstants.VERTICAL, -1, false)); + map.put("unitScrollDown", new PScrollAction("UnitScrollDown", SwingConstants.VERTICAL, 1, false)); + + map.put("scrollEnd", new PScrollEndAction("ScrollEnd")); + map.put("scrollHome", new PScrollHomeAction("ScrollHome")); + } + + /** + * Disables key actions on this PScrollPane. + */ + protected void disableKeyActions() { + final ActionMap map = getActionMap(); + + if (nullAction == null) { + nullAction = new PNullAction(); + } + + map.put("scrollUp", nullAction); + map.put("scrollDown", nullAction); + map.put("scrollLeft", nullAction); + map.put("scrollRight", nullAction); + map.put("unitScrollRight", nullAction); + map.put("unitScrollLeft", nullAction); + map.put("unitScrollUp", nullAction); + map.put("unitScrollDown", nullAction); + map.put("scrollEnd", nullAction); + map.put("scrollHome", nullAction); + } + + /** + * Overridden to create the Piccolo2D viewport. + * + * @return the Piccolo2D version of the viewport + */ + protected JViewport createViewport() { + return new PViewport(); + } + + /** + * Action to scroll left/right/up/down. Modified from + * javax.swing.plaf.basic.BasicScrollPaneUI.ScrollAction. + * + * Gets the view parameters (position and size) from the Viewport rather + * than directly from the view - also only performs its actions when the + * relevant scrollbar is visible. + */ + protected static class PScrollAction extends AbstractAction { + private static final int MINIMUM_SCROLL_SIZE = 10; + private static final long serialVersionUID = 1L; + /** Direction to scroll. */ + protected int orientation; + /** 1 indicates scroll down, -1 up. */ + protected int direction; + /** True indicates a block scroll, otherwise a unit scroll. */ + private final boolean block; + + /** + * Constructs a scroll action with the given name in the given + * orientiation stated and in the direction provided. + * + * @param name arbitrary name of action + * @param orientation horizontal or vertical + * @param direction 1 indicates scroll down, -1 up + * @param block true if block scroll as opposed to unit + */ + protected PScrollAction(final String name, final int orientation, final int direction, final boolean block) { + super(name); + this.orientation = orientation; + this.direction = direction; + this.block = block; + } + + /** + * Performs the scroll action if the action was performed on visible + * scrollbars and if the viewport is valid. + * + * @param event the event responsible for this action being performed + */ + public void actionPerformed(final ActionEvent event) { + final JScrollPane scrollpane = (JScrollPane) event.getSource(); + if (!isScrollEventOnVisibleScrollbars(scrollpane)) { + return; + } + + final JViewport vp = scrollpane.getViewport(); + if (vp == null) { + return; + } + + Component view = vp.getView(); + if (view == null) { + return; + } + + final Rectangle visRect = vp.getViewRect(); + // LEG: Modification to query the viewport for the + // view size rather than going directly to the view + final Dimension vSize = vp.getViewSize(); + final int amount; + + if (view instanceof Scrollable) { + if (block) { + amount = ((Scrollable) view).getScrollableBlockIncrement(visRect, orientation, direction); + } + else { + amount = ((Scrollable) view).getScrollableUnitIncrement(visRect, orientation, direction); + } + } + else { + if (block) { + if (orientation == SwingConstants.VERTICAL) { + amount = visRect.height; + } + else { + amount = visRect.width; + } + } + else { + amount = MINIMUM_SCROLL_SIZE; + } + } + + if (orientation == SwingConstants.VERTICAL) { + visRect.y += amount * direction; + if (visRect.y + visRect.height > vSize.height) { + visRect.y = Math.max(0, vSize.height - visRect.height); + } + else if (visRect.y < 0) { + visRect.y = 0; + } + } + else { + visRect.x += amount * direction; + if (visRect.x + visRect.width > vSize.width) { + visRect.x = Math.max(0, vSize.width - visRect.width); + } + else if (visRect.x < 0) { + visRect.x = 0; + } + } + vp.setViewPosition(visRect.getLocation()); + } + + private boolean isScrollEventOnVisibleScrollbars(final JScrollPane scrollpane) { + return orientation == SwingConstants.VERTICAL && scrollpane.getVerticalScrollBar().isShowing() + || orientation == SwingConstants.HORIZONTAL && scrollpane.getHorizontalScrollBar().isShowing(); + } + } + + /** + * Action to scroll to x,y location of 0,0. Modified from + * javax.swing.plaf.basic.BasicScrollPaneUI.ScrollEndAction. + * + * Only performs the event if a scrollbar is visible. + */ + private static class PScrollHomeAction extends AbstractAction { + private static final long serialVersionUID = 1L; + + protected PScrollHomeAction(final String name) { + super(name); + } + + public void actionPerformed(final ActionEvent e) { + final JScrollPane scrollpane = (JScrollPane) e.getSource(); + // LEG: Modification to only perform these actions if one of the + // scrollbars is actually showing + if (scrollpane.getVerticalScrollBar().isShowing() || scrollpane.getHorizontalScrollBar().isShowing()) { + final JViewport vp = scrollpane.getViewport(); + if (vp != null && vp.getView() != null) { + vp.setViewPosition(new Point(0, 0)); + } + } + } + } + + /** + * Action to scroll to last visible location. Modified from + * javax.swing.plaf.basic.BasicScrollPaneUI.ScrollEndAction. + * + * Gets the view size from the viewport rather than directly from the view - + * also only performs the event if a scrollbar is visible. + */ + protected static class PScrollEndAction extends AbstractAction { + private static final long serialVersionUID = 1L; + + /** + * Constructs a scroll to end action with the given name. + * + * @param name name to assign to this action + */ + protected PScrollEndAction(final String name) { + super(name); + } + + /** + * Scrolls to the end of the viewport if there are visible scrollbars. + * + * @param event event responsible for the scroll event + */ + public void actionPerformed(final ActionEvent event) { + final JScrollPane scrollpane = (JScrollPane) event.getSource(); + // LEG: Modification to only perform these actions if one of the + // scrollbars is actually showing + if (scrollpane.getVerticalScrollBar().isShowing() || scrollpane.getHorizontalScrollBar().isShowing()) { + + final JViewport vp = scrollpane.getViewport(); + if (vp != null && vp.getView() != null) { + + final Rectangle visRect = vp.getViewRect(); + // LEG: Modification to query the viewport for the + // view size rather than going directly to the view + final Dimension size = vp.getViewSize(); + vp.setViewPosition(new Point(size.width - visRect.width, size.height - visRect.height)); + } + } + } + } + + /** + * An action to do nothing - put into an action map to keep it from looking + * to its parent. + */ + protected static class PNullAction extends AbstractAction { + private static final long serialVersionUID = 1L; + + /** + * Does nothing. + * + * @param e Event responsible for this action + */ + public void actionPerformed(final ActionEvent e) { + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/swing/PScrollPaneLayout.java b/src/main/java/edu/umd/cs/piccolox/swing/PScrollPaneLayout.java new file mode 100644 index 0000000..fb354b6 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/swing/PScrollPaneLayout.java @@ -0,0 +1,416 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.swing; + +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Rectangle; + +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneLayout; +import javax.swing.border.Border; + +import edu.umd.cs.piccolo.util.PBounds; + +/** + * A subclass of ScrollPaneLayout that looks at the Viewport for sizing + * information rather than View. Also queries the Viewport for sizing + * information after each decision about scrollbar visiblity. + * + * @author Lance Good + */ +public class PScrollPaneLayout extends ScrollPaneLayout { + private static final long serialVersionUID = 1L; + + /** + * MODIFIED FROM javax.swing.ScrollPaneLayout.layoutContainer. + * + * This is largely the same as ScrollPaneLayout.layoutContainer but obtains + * the preferred view size from the viewport rather than directly from the + * view so the viewport can get the preferred size from the PScrollDirector + * + * @param parent the Container to lay out + */ + public void layoutContainer(final Container parent) { + // Sync the (now obsolete) policy fields with the JScrollPane. + if (!(parent instanceof JScrollPane)) { + throw new IllegalArgumentException("layoutContainer may only be applied to JScrollPanes"); + } + final JScrollPane scrollPane = (JScrollPane) parent; + vsbPolicy = scrollPane.getVerticalScrollBarPolicy(); + hsbPolicy = scrollPane.getHorizontalScrollBarPolicy(); + + final Rectangle availR = scrollPane.getBounds(); + availR.setLocation(0, 0); + + final Insets insets = parent.getInsets(); + availR.x = insets.left; + availR.y = insets.top; + availR.width -= insets.left + insets.right; + availR.height -= insets.top + insets.bottom; + + // Get the scrollPane's orientation. + final boolean leftToRight = scrollPane.getComponentOrientation().isLeftToRight(); + + /* + * If there's a visible column header remove the space it needs from the + * top of availR. The column header is treated as if it were fixed + * height, arbitrary width. + */ + final Rectangle colHeadR = new Rectangle(0, availR.y, 0, 0); + + if (colHead != null && colHead.isVisible()) { + final int colHeadHeight = colHead.getPreferredSize().height; + colHeadR.height = colHeadHeight; + availR.y += colHeadHeight; + availR.height -= colHeadHeight; + } + + /* + * If there's a visible row header remove the space it needs from the + * left or right of availR. The row header is treated as if it were + * fixed width, arbitrary height. + */ + final Rectangle rowHeadR = new Rectangle(0, 0, 0, 0); + + if (rowHead != null && rowHead.isVisible()) { + final int rowHeadWidth = rowHead.getPreferredSize().width; + rowHeadR.width = rowHeadWidth; + availR.width -= rowHeadWidth; + if (leftToRight) { + rowHeadR.x = availR.x; + availR.x += rowHeadWidth; + } + else { + rowHeadR.x = availR.x + availR.width; + } + } + + /* + * If there's a JScrollPane.viewportBorder, remove the space it occupies + * for availR. + */ + final Border viewportBorder = scrollPane.getViewportBorder(); + Insets vpbInsets; + if (viewportBorder != null) { + vpbInsets = viewportBorder.getBorderInsets(parent); + availR.x += vpbInsets.left; + availR.y += vpbInsets.top; + availR.width -= vpbInsets.left + vpbInsets.right; + availR.height -= vpbInsets.top + vpbInsets.bottom; + } + else { + vpbInsets = new Insets(0, 0, 0, 0); + } + + /* + * At this point availR is the space available for the viewport and + * scrollbars. rowHeadR is correct except for its height and y and + * colHeadR is correct except for its width and x. Once we're through + * computing the dimensions of these three parts we can go back and set + * the dimensions of rowHeadR.height, rowHeadR.y, colHeadR.width, + * colHeadR.x and the bounds for the corners. + * + * We'll decide about putting up scrollbars by comparing the viewport + * views preferred size with the viewports extent size (generally just + * its size). Using the preferredSize is reasonable because layout + * proceeds top down - so we expect the viewport to be layed out next. + * And we assume that the viewports layout manager will give the view + * it's preferred size. + */ + Dimension extentSize = getExtentSize(availR); + + final PBounds cameraBounds = new PBounds(0, 0, extentSize.getWidth(), extentSize.getHeight()); + + // LEG: Modification to ask the viewport for the view size rather + // than asking the view directly + Dimension viewPrefSize = getViewSize(cameraBounds); + + /* + * If there's a vertical scrollbar and we need one, allocate space for + * it (we'll make it visible later). A vertical scrollbar is considered + * to be fixed width, arbitrary height. + */ + final Rectangle vsbR = new Rectangle(0, availR.y - vpbInsets.top, 0, 0); + + boolean vsbNeeded; + if (vsbPolicy == VERTICAL_SCROLLBAR_ALWAYS) { + vsbNeeded = true; + } + else if (vsbPolicy == VERTICAL_SCROLLBAR_NEVER) { + vsbNeeded = false; + } + else { // vsbPolicy == VERTICAL_SCROLLBAR_AS_NEEDED + + vsbNeeded = viewPrefSize.height > extentSize.height; + } + + if (vsb != null && vsbNeeded) { + adjustForVSB(true, availR, vsbR, vpbInsets, leftToRight); + extentSize = viewport.toViewCoordinates(availR.getSize()); + + // LEG: Modification because the view's preferred size needs to + // be recomputed because the extent may have changed + cameraBounds.setRect(0, 0, extentSize.getWidth(), extentSize.getHeight()); + viewPrefSize = ((PViewport) viewport).getViewSize(cameraBounds); + } + + /* + * If there's a horizontal scrollbar and we need one, allocate space for + * it (we'll make it visible later). A horizontal scrollbar is + * considered to be fixed height, arbitrary width. + */ + final Rectangle hsbR = new Rectangle(availR.x - vpbInsets.left, 0, 0, 0); + boolean hsbNeeded; + if (hsbPolicy == HORIZONTAL_SCROLLBAR_ALWAYS) { + hsbNeeded = true; + } + else if (hsbPolicy == HORIZONTAL_SCROLLBAR_NEVER) { + hsbNeeded = false; + } + else { // hsbPolicy == HORIZONTAL_SCROLLBAR_AS_NEEDED + hsbNeeded = viewPrefSize.width > extentSize.width; + } + + if (hsb != null && hsbNeeded) { + adjustForHSB(true, availR, hsbR, vpbInsets); + + /* + * If we added the horizontal scrollbar then we've implicitly + * reduced the vertical space available to the viewport. As a + * consequence we may have to add the vertical scrollbar, if that + * hasn't been done so already. Ofcourse we don't bother with any of + * this if the vsbPolicy is NEVER. + */ + if (vsb != null && !vsbNeeded && vsbPolicy != VERTICAL_SCROLLBAR_NEVER) { + + extentSize = viewport.toViewCoordinates(availR.getSize()); + + // LEG: Modification because the view's preferred size needs to + // be recomputed because the extent may have changed + cameraBounds.setRect(0, 0, extentSize.getWidth(), extentSize.getHeight()); + viewPrefSize = ((PViewport) viewport).getViewSize(cameraBounds); + + vsbNeeded = viewPrefSize.height > extentSize.height; + + if (vsbNeeded) { + adjustForVSB(true, availR, vsbR, vpbInsets, leftToRight); + } + } + } + + /* + * Set the size of the viewport first, and then recheck the Scrollable + * methods. Some components base their return values for the Scrollable + * methods on the size of the Viewport, so that if we don't ask after + * resetting the bounds we may have gotten the wrong answer. + */ + if (viewport != null) { + viewport.setBounds(availR); + } + + /* + * We now have the final size of the viewport: availR. Now fixup the + * header and scrollbar widths/heights. + */ + vsbR.height = availR.height + vpbInsets.top + vpbInsets.bottom; + hsbR.width = availR.width + vpbInsets.left + vpbInsets.right; + rowHeadR.height = availR.height + vpbInsets.top + vpbInsets.bottom; + rowHeadR.y = availR.y - vpbInsets.top; + colHeadR.width = availR.width + vpbInsets.left + vpbInsets.right; + colHeadR.x = availR.x - vpbInsets.left; + + /* + * Set the bounds of the remaining components. The scrollbars are made + * invisible if they're not needed. + */ + if (rowHead != null) { + rowHead.setBounds(rowHeadR); + } + + if (colHead != null) { + colHead.setBounds(colHeadR); + } + + if (vsb != null) { + if (vsbNeeded) { + vsb.setVisible(true); + vsb.setBounds(vsbR); + } + else { + vsb.setVisible(false); + } + } + + if (hsb != null) { + if (hsbNeeded) { + hsb.setVisible(true); + hsb.setBounds(hsbR); + } + else { + hsb.setVisible(false); + } + } + + if (lowerLeft != null) { + if (leftToRight) { + lowerLeft.setBounds(rowHeadR.x, hsbR.y, rowHeadR.width, hsbR.height); + } + else { + lowerLeft.setBounds(vsbR.x, hsbR.y, vsbR.width, hsbR.height); + } + } + + if (lowerRight != null) { + if (leftToRight) { + lowerRight.setBounds(vsbR.x, hsbR.y, vsbR.width, hsbR.height); + } + else { + lowerRight.setBounds(rowHeadR.x, hsbR.y, rowHeadR.width, hsbR.height); + } + } + + if (upperLeft != null) { + if (leftToRight) { + upperLeft.setBounds(rowHeadR.x, colHeadR.y, rowHeadR.width, colHeadR.height); + } + else { + upperLeft.setBounds(vsbR.x, colHeadR.y, vsbR.width, colHeadR.height); + } + } + + if (upperRight != null) { + if (leftToRight) { + upperRight.setBounds(vsbR.x, colHeadR.y, vsbR.width, colHeadR.height); + } + else { + upperRight.setBounds(rowHeadR.x, colHeadR.y, rowHeadR.width, colHeadR.height); + } + } + } + + /** + * @param cameraBounds + * @return + */ + private Dimension getViewSize(final PBounds cameraBounds) { + Dimension viewPrefSize; + if (viewport != null) { + viewPrefSize = ((PViewport) viewport).getViewSize(cameraBounds); + } + else { + viewPrefSize = new Dimension(0, 0); + } + return viewPrefSize; + } + + /** + * @param availR + * @return + */ + private Dimension getExtentSize(final Rectangle availR) { + Dimension extentSize; + if (viewport != null) { + extentSize = viewport.toViewCoordinates(availR.getSize()); + } + else { + extentSize = new Dimension(0, 0); + } + return extentSize; + } + + /** + * Copied FROM javax.swing.ScrollPaneLayout.adjustForVSB. + * + * This method is called from ScrollPaneLayout.layoutContainer and is + * private in ScrollPaneLayout so it was copied here + * + * @param wantsVSB whether to account for vertical scrollbar + * @param available region to adjust + * @param vsbR vertical scroll bar region + * @param vpbInsets margin of vertical scroll bars + * @param leftToRight orientation of the text LTR or RTL + */ + protected void adjustForVSB(final boolean wantsVSB, final Rectangle available, final Rectangle vsbR, + final Insets vpbInsets, final boolean leftToRight) { + final int vsbWidth = vsb.getPreferredSize().width; + if (wantsVSB) { + available.width -= vsbWidth; + vsbR.width = vsbWidth; + + if (leftToRight) { + vsbR.x = available.x + available.width + vpbInsets.right; + } + else { + vsbR.x = available.x - vpbInsets.left; + available.x += vsbWidth; + } + } + else { + available.width += vsbWidth; + } + } + + /** + * Copied FROM javax.swing.ScrollPaneLayout.adjustForHSB. + * + * This method is called from ScrollPaneLayout.layoutContainer and is + * private in ScrollPaneLayout so it was copied here + * + * @param wantsHSB whether to account for horizontal scrollbar + * @param available region to adjust + * @param hsbR vertical scroll bar region + * @param vpbInsets margin of the scroll bars + */ + protected void adjustForHSB(final boolean wantsHSB, final Rectangle available, final Rectangle hsbR, + final Insets vpbInsets) { + final int hsbHeight = hsb.getPreferredSize().height; + if (wantsHSB) { + available.height -= hsbHeight; + hsbR.y = available.y + available.height + vpbInsets.bottom; + hsbR.height = hsbHeight; + } + else { + available.height += hsbHeight; + } + } + + /** + * The UI resource version of PScrollPaneLayout. It isn't clear why Swing + * does this in ScrollPaneLayout but we'll do it here too just to be safe. + */ + public static class UIResource extends PScrollPaneLayout implements javax.swing.plaf.UIResource { + + /** + * + */ + private static final long serialVersionUID = 1L; + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/swing/PViewport.java b/src/main/java/edu/umd/cs/piccolox/swing/PViewport.java new file mode 100644 index 0000000..d37d7f3 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/swing/PViewport.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.swing; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.LayoutManager; +import java.awt.Point; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import javax.swing.JViewport; +import javax.swing.ViewportLayout; + +import edu.umd.cs.piccolo.PCanvas; +import edu.umd.cs.piccolo.util.PBounds; + +/** + * A subclass of JViewport that talks to the scroll director to negotiate the + * view positions and sizes. + * + * @author Lance Good + */ +public class PViewport extends JViewport { + private static final long serialVersionUID = 1L; + /** Controls what happens when scrolling occurs. */ + PScrollDirector scrollDirector; + + /** Pass constructor info to super. */ + public PViewport() { + super(); + + setScrollDirector(createScrollDirector()); + } + + /** + * Subclasses can override this to install a different layout manager (or + *null
) in the constructor. Returns a new
+ * ViewportLayout
object.
+ *
+ * @return a LayoutManager
+ */
+ protected LayoutManager createLayoutManager() {
+ return new PViewportLayout();
+ }
+
+ /**
+ * Subclasses can override this to install a different scroll director in
+ * the constructor. Returns a new PScrollDirector
object.
+ *
+ * @return a PScrollDirector
+ */
+ protected PScrollDirector createScrollDirector() {
+ return new PDefaultScrollDirector();
+ }
+
+ /**
+ * Set the scroll director on this viewport.
+ *
+ * @param scrollDirector The new scroll director
+ */
+ public void setScrollDirector(final PScrollDirector scrollDirector) {
+ if (this.scrollDirector != null) {
+ this.scrollDirector.unInstall();
+ }
+ this.scrollDirector = scrollDirector;
+ if (scrollDirector != null) {
+ this.scrollDirector.install(this, (PCanvas) getView());
+ }
+ }
+
+ /**
+ * Returns the scroll director on this viewport.
+ *
+ * @return The scroll director on this viewport
+ */
+ public PScrollDirector getScrollDirector() {
+ return scrollDirector;
+ }
+
+ /**
+ * Overridden to throw an exception if the view is not a PCanvas.
+ *
+ * @param view The new view - it better be a ZCanvas!
+ */
+ public void setView(final Component view) {
+ if (!(view instanceof PCanvas)) {
+ throw new UnsupportedOperationException("PViewport only supports ZCanvas");
+ }
+
+ super.setView(view);
+
+ if (scrollDirector != null) {
+ scrollDirector.install(this, (PCanvas) view);
+ }
+ }
+
+ /**
+ * Notifies all ChangeListeners
when the views size, position,
+ * or the viewports extent size has changed.
+ *
+ * PDefaultScrollDirector calls this so it needs to be public.
+ */
+ public void fireStateChanged() {
+ super.fireStateChanged();
+ }
+
+ /**
+ * Sets the view coordinates that appear in the upper left hand corner of
+ * the viewport, does nothing if there's no view.
+ *
+ * @param p a Point object giving the upper left coordinates
+ */
+ public void setViewPosition(final Point p) {
+ if (getView() == null) {
+ return;
+ }
+
+ double oldX = 0, oldY = 0;
+ final double x = p.x, y = p.y;
+
+ final Point2D vp = getViewPosition();
+ if (vp != null) {
+ oldX = vp.getX();
+ oldY = vp.getY();
+ }
+
+ // Send the scroll director the exact view position and let it interpret
+ // it as needed
+ final double newX = x;
+ final double newY = y;
+
+ if (oldX != newX || oldY != newY) {
+ scrollUnderway = true;
+
+ scrollDirector.setViewPosition(newX, newY);
+
+ fireStateChanged();
+ }
+ }
+
+ /**
+ * Gets the view position from the scroll director based on the current
+ * extent size.
+ *
+ * @return The new view's position
+ */
+ public Point getViewPosition() {
+ if (scrollDirector == null) {
+ return null;
+ }
+
+ final Dimension extent = getExtentSize();
+ return scrollDirector.getViewPosition(new PBounds(0, 0, extent.getWidth(), extent.getHeight()));
+ }
+
+ /**
+ * Gets the view size from the scroll director based on the current extent
+ * size.
+ *
+ * @return The new view size
+ */
+ public Dimension getViewSize() {
+ final Dimension extent = getExtentSize();
+ return scrollDirector.getViewSize(new PBounds(0, 0, extent.getWidth(), extent.getHeight()));
+ }
+
+ /**
+ * Gets the view size from the scroll director based on the specified extent
+ * size.
+ *
+ * @param r The extent size from which the view is computed
+ * @return The new view size
+ */
+ public Dimension getViewSize(final Rectangle2D r) {
+ return scrollDirector.getViewSize(r);
+ }
+
+ /**
+ * A simple layout manager to give the ZCanvas the same size as the Viewport.
+ */
+ public static class PViewportLayout extends ViewportLayout {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Called when the specified container needs to be laid out.
+ *
+ * @param parent the container to lay out
+ */
+ public void layoutContainer(final Container parent) {
+ if (!(parent instanceof JViewport)) {
+ throw new IllegalArgumentException("PViewport.layoutContainer may only be applied to JViewports");
+ }
+ final JViewport vp = (JViewport) parent;
+ final Component view = vp.getView();
+
+ if (view == null) {
+ return;
+ }
+
+ final Dimension extentSize = vp.getSize();
+
+ vp.setViewSize(extentSize);
+ }
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/swing/SwingLayoutNode.java b/src/main/java/edu/umd/cs/piccolox/swing/SwingLayoutNode.java
new file mode 100644
index 0000000..a02c7ee
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/swing/SwingLayoutNode.java
@@ -0,0 +1,655 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.swing;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.LayoutManager;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Collection;
+import java.util.Iterator;
+
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+
+import edu.umd.cs.piccolo.PNode;
+
+/**
+ * Uses Swing layout managers to position PNodes.
+ *
+ * @author Sam Reid
+ * @author Chris Malley (cmalley@pixelzoom.com)
+ */
+public class SwingLayoutNode extends PNode {
+ private static final long serialVersionUID = 1L;
+ /*
+ * How the space allocated by the Swing layout manager is used differs
+ * depending on Swing component type. The behavior of a default JLabel
+ * (Anchors.WEST) seems to make the most sense for PNodes.
+ */
+ private static final Anchor DEFAULT_ANCHOR = Anchor.WEST;
+
+ /** Container for ProxyComponents. */
+ private final Container container;
+
+ private final PropertyChangeListener propertyChangeListener;
+
+ /** Anchor to use when adding child nodes and they don't specify one. */
+ private Anchor defaultAnchor;
+
+ /**
+ * Construct a SwingLayoutNode that uses FlowLayout.
+ */
+ public SwingLayoutNode() {
+ this(new FlowLayout());
+ }
+
+ /**
+ * Constructs a SwingLayoutNode that uses the provided LayoutManager to
+ * layout its children.
+ *
+ * @param layoutManager LayoutManager to use for laying out children. Must
+ * not be null.
+ */
+ public SwingLayoutNode(final LayoutManager layoutManager) {
+ this(new JPanel(layoutManager));
+ }
+
+ /**
+ * Constructs a SwingLayoutNode that lays out its children as though they
+ * were children of the provided Container.
+ *
+ * Whatever LayoutManager is being used by the container will be used when
+ * laying out nodes.
+ *
+ * @param container Container in which child nodes will effectively be laid
+ * out
+ */
+ public SwingLayoutNode(Container container) {
+ this.container = container;
+ propertyChangeListener = new PropertyChangeListener() {
+ public void propertyChange(final PropertyChangeEvent event) {
+ final String propertyName = event.getPropertyName();
+ if (isLayoutProperty(propertyName)) {
+ updateContainerLayout();
+ }
+ }
+ };
+ defaultAnchor = DEFAULT_ANCHOR;
+ }
+
+ /**
+ * Sets the default anchor. If no anchor is specified when a node is added,
+ * then the default anchor determines where the node is positioned in the
+ * space allocated by the Swing layout manager.
+ *
+ * @param anchor anchor to use when a node is added but its anchor is not
+ * specified
+ */
+ public void setAnchor(final Anchor anchor) {
+ this.defaultAnchor = anchor;
+ }
+
+ /**
+ * Returns the anchor being used by this LayoutManager.
+ *
+ * @return anchor currently being used when laying out children.
+ */
+ public Anchor getAnchor() {
+ return defaultAnchor;
+ }
+
+ /**
+ * Some Swing layout managers (like BoxLayout) require a reference to the
+ * proxy Container.
+ *
+ * For example:
+ * SwingLayoutNode layoutNode = new SwingLayoutNode();
+ * layoutNode.setLayout( new BoxLayout( layoutNode.getContainer(), BoxLayout.Y_AXIS ) );
+ *
+ *
+ * @return container in which children will logically be laid out in
+ */
+ public Container getContainer() {
+ return container;
+ }
+
+ /**
+ * Adds a child at the specified index. Like Swing, bad things can happen if
+ * the type of the constraints isn't compatible with the layout manager.
+ *
+ * @param index 0 based index at which to add the child
+ * @param child child to be added
+ * @param constraints constraints the layout manager uses when laying out
+ * the child
+ * @param childAnchor specifies the location from which layout takes place
+ */
+ public void addChild(final int index, final PNode child, final Object constraints, final Anchor childAnchor) {
+ /*
+ * NOTE: This must be the only super.addChild call that we make in our
+ * entire implementation, because all PNode.addChild methods are
+ * implemented in terms of this one. Calling other variants of
+ * super.addChild will incorrectly invoke our overrides, resulting in
+ * StackOverflowException.
+ */
+ super.addChild(index, child);
+ addProxyComponent(child, constraints, childAnchor);
+ }
+
+ /** {@inheritDoc} */
+ public void addChild(final int index, final PNode child) {
+ addChild(index, child, null, defaultAnchor);
+ }
+
+ /**
+ * Adds a child at the specified index. Like Swing, bad things can happen if
+ * the type of the constraints isn't compatible with the layout manager.
+ *
+ * @param index 0 based index at which to add the child
+ * @param child child to be added
+ * @param constraints constraints the layout manager uses when laying out
+ * the child
+ */
+ public void addChild(final int index, final PNode child, final Object constraints) {
+ addChild(index, child, constraints, defaultAnchor);
+ }
+
+ /**
+ * Adds a child at the specified index.
+ *
+ * @param index 0 based index at which to add the child
+ * @param child child to be added
+ * @param anchor specifies the location from which layout takes place
+ */
+ public void addChild(final int index, final PNode child, final Anchor anchor) {
+ addChild(index, child, null, anchor);
+ }
+
+ /**
+ * Adds a child to the end of the node list.
+ *
+ * @param child child to be added
+ * @param constraints constraints the layout manager uses when laying out
+ * the child
+ * @param anchor specifies the location from which layout takes place
+ */
+ public void addChild(final PNode child, final Object constraints, final Anchor anchor) {
+ // NOTE: since PNode.addChild(PNode) is implemented in terms of
+ // PNode.addChild(int index), we must do the same.
+ int index = getChildrenCount();
+ // workaround a flaw in PNode.addChild(PNode), they should have handled
+ // this in PNode.addChild(int index).
+ if (child.getParent() == this) {
+ index--;
+ }
+ addChild(index, child, constraints, anchor);
+ }
+
+ /**
+ * Adds a child to the end of the node list.
+ *
+ * @param child child to be added
+ */
+ public void addChild(final PNode child) {
+ addChild(child, null, defaultAnchor);
+ }
+
+ /**
+ * Adds a child to the end of the node list and specifies the given
+ * constraints.
+ *
+ * @param child child to be added
+ * @param constraints constraints the layout manager uses when laying out
+ * the child
+ */
+ public void addChild(final PNode child, final Object constraints) {
+ addChild(child, constraints, defaultAnchor);
+ }
+
+ /**
+ * Adds a child to the end of the node list.
+ *
+ * @param child child to be added
+ * @param anchor specifies the location from which layout takes place
+ */
+ public void addChild(final PNode child, final Anchor anchor) {
+ addChild(child, null, anchor);
+ }
+
+ /**
+ * Adds a collection of nodes to the end of the list.
+ *
+ * @param nodes nodes to add to the end of the list
+ * @param constraints constraints the layout manager uses when laying out
+ * the child
+ * @param anchor specifies the location from which layout takes place
+ */
+ public void addChildren(final Collection nodes, final Object constraints, final Anchor anchor) {
+ final Iterator i = nodes.iterator();
+ while (i.hasNext()) {
+ final PNode each = (PNode) i.next();
+ addChild(each, constraints, anchor);
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void addChildren(final Collection nodes) {
+ addChildren(nodes, null, defaultAnchor);
+ }
+
+ /**
+ * Adds a collection of nodes to the end of the list.
+ *
+ * @param nodes nodes to add to the end of the list
+ * @param constraints constraints the layout manager uses when laying out
+ * the child
+ */
+ public void addChildren(final Collection nodes, final Object constraints) {
+ addChildren(nodes, constraints, defaultAnchor);
+ }
+
+ /**
+ * Adds a collection of nodes to the end of the list.
+ *
+ * @param nodes nodes to add to the end of the list
+ * @param anchor specifies the location from which layout takes place
+ */
+ public void addChildren(final Collection nodes, final Anchor anchor) {
+ addChildren(nodes, null, anchor);
+ }
+
+ /**
+ * Removes a node at a specified index.
+ *
+ * @param index 0 based index of the child to be removed
+ */
+ public PNode removeChild(final int index) {
+ /*
+ * NOTE: This must be the only super.removeChild call that we make in
+ * our entire implementation, because all PNode.removeChild methods are
+ * implemented in terms of this one. Calling other variants of
+ * super.removeChild will incorrectly invoke our overrides, resulting in
+ * StackOverflowException.
+ */
+ final PNode node = super.removeChild(index);
+ removeProxyComponent(node);
+ return node;
+ }
+
+ /*
+ * NOTE We don't need to override removeChild(PNode) or removeChildren,
+ * because they call removeChild(int index). If their implementation ever
+ * changes, then we'll need to override them.
+ */
+
+ /**
+ * PNode.removeAllChildren does not call removeChild, it manipulates an
+ * internal data structure. So we must override this in a more careful (and
+ * less efficient) manner.
+ */
+ public void removeAllChildren() {
+ final Iterator i = getChildrenIterator();
+ while (i.hasNext()) {
+ removeChild((PNode) i.next());
+ }
+ }
+
+ /**
+ * Adds a proxy component for a node.
+ *
+ * @param node node for which to add the proxy component
+ * @param constraints Constraints to apply when laying out the component
+ * @param anchor relative anchor point of the underyling proxy component on
+ * its container
+ */
+ private void addProxyComponent(final PNode node, final Object constraints, final Anchor anchor) {
+ final ProxyComponent component = new ProxyComponent(node, anchor);
+ container.add(component, constraints);
+ node.addPropertyChangeListener(propertyChangeListener);
+ updateContainerLayout();
+ }
+
+ /**
+ * Removes a proxy component for a node. Does nothing if the node is not a
+ * child of the layout.
+ *
+ * @param node node from which the proxy container should be removed from.
+ */
+ private void removeProxyComponent(final PNode node) {
+ if (node != null) {
+ final ProxyComponent component = getComponentForNode(node);
+ if (component != null) {
+ container.remove(component);
+ node.removePropertyChangeListener(propertyChangeListener);
+ updateContainerLayout();
+ }
+ }
+ }
+
+ /**
+ * Finds the component that is serving as the proxy for a specific node.
+ * Returns null if not found.
+ */
+ private ProxyComponent getComponentForNode(final PNode node) {
+ ProxyComponent nodeComponent = null;
+ final Component[] components = container.getComponents();
+ if (components != null) {
+ for (int i = 0; i < components.length && nodeComponent == null; i++) {
+ if (components[i] instanceof ProxyComponent) {
+ final ProxyComponent n = (ProxyComponent) components[i];
+ if (n.getNode() == node) {
+ nodeComponent = n;
+ }
+ }
+ }
+ }
+ return nodeComponent;
+ }
+
+ /**
+ * Helper to figure out if the given property name relates to layout.
+ *
+ * @param propertyName name of property being tested
+ *
+ * @return true property name relates to layout.
+ */
+ private boolean isLayoutProperty(final String propertyName) {
+ return propertyName.equals(PNode.PROPERTY_VISIBLE) || propertyName.equals(PNode.PROPERTY_FULL_BOUNDS) ||
+ propertyName.equals(PNode.PROPERTY_BOUNDS) || propertyName.equals(PNode.PROPERTY_TRANSFORM);
+ }
+
+ /**
+ * Updates the Proxy Container's layout.
+ */
+ private void updateContainerLayout() {
+ container.invalidate(); // necessary for layouts like BoxLayout that
+ // would otherwise use stale state
+ container.setSize(container.getPreferredSize());
+ container.doLayout();
+ }
+
+ /**
+ * JComponent that acts as a proxy for a PNode. Provides the PNode's bounds
+ * info for all bounds-related requests.
+ */
+ private static class ProxyComponent extends JComponent {
+ private static final long serialVersionUID = 1L;
+ private final PNode node;
+ private final Anchor anchor;
+
+ public ProxyComponent(final PNode node, final Anchor anchor) {
+ this.node = node;
+ this.anchor = anchor;
+ }
+
+ /**
+ * Returns the associated PNode.
+ *
+ * @return associated PNode
+ */
+ public PNode getNode() {
+ return node;
+ }
+
+ /**
+ * Report the node's dimensions as the ProxyComponent's preferred size.
+ */
+ public Dimension getPreferredSize() {
+ // Round up fractional part instead of rounding down; better to
+ // include the whole node than to chop off part.
+ final double w = node.getFullBoundsReference().getWidth();
+ final double h = node.getFullBoundsReference().getHeight();
+ return new Dimension(roundUp(w), roundUp(h));
+ }
+
+ private int roundUp(final double val) {
+ return (int) Math.ceil(val);
+ }
+
+ /**
+ * Return the PNode size as the minimum dimension; required by layouts
+ * such as BoxLayout.
+ *
+ * @return the minimum size for this component
+ */
+ public Dimension getMinimumSize() {
+ return getPreferredSize();
+ }
+
+ /**
+ * Sets the bounds of the ProxyComponent and positions the node in the
+ * area (x,y,w,h) allocated by the layout manager.
+ */
+ public void setBounds(final int x, final int y, final int w, final int h) {
+ // important to check that the bounds have really changed, or we'll
+ // cause StackOverflowException
+ if (x != getX() || y != getY() || w != getWidth() || h != getHeight()) {
+ super.setBounds(x, y, w, h);
+ anchor.positionNode(node, x, y, w, h);
+ }
+ }
+ }
+
+ /**
+ * Determines where nodes are anchored in the area allocated by the Swing
+ * layout manager. Predefined anchor names are similar to GridBagConstraint
+ * anchors and have the same semantics.
+ */
+ public interface Anchor {
+
+ /**
+ * Positions the node in the bounds defined.
+ *
+ * @param node node to be laid out
+ * @param x left of bounds
+ * @param y top of bounds
+ * @param width width of bounds
+ * @param height height of bounds
+ */
+ void positionNode(PNode node, double x, double y, double width, double height);
+
+ /**
+ * Base class that provides utilities for computing common anchor
+ * points.
+ */
+
+ /** Anchors the node's center as the point used when laying it out. */
+ static final Anchor CENTER = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(centerX(node, x, w), centerY(node, y, h));
+ }
+ };
+
+ /** Anchors the node's top center as the point used when laying it out. */
+ static final Anchor NORTH = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(centerX(node, x, w), north(node, y, h));
+ }
+ };
+
+ /** Anchors the node's top right as the point used when laying it out. */
+ static final Anchor NORTHEAST = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(east(node, x, w), north(node, y, h));
+ }
+ };
+
+ /**
+ * Anchors the node's middle right as the point used when laying it out.
+ */
+ static final Anchor EAST = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(east(node, x, w), centerY(node, y, h));
+ }
+ };
+
+ /**
+ * Anchors the node's bottom right as the point used when laying it out.
+ */
+ static final Anchor SOUTHEAST = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(east(node, x, w), south(node, y, h));
+ }
+ };
+
+ /**
+ * Anchors the node's center bottom as the point used when laying it
+ * out.
+ */
+ static final Anchor SOUTH = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(centerX(node, x, w), south(node, y, h));
+ }
+ };
+
+ /** Anchors the node's bottom left as the point used when laying it out. */
+ static final Anchor SOUTHWEST = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(west(node, x, w), south(node, y, h));
+ }
+ };
+
+ /** Anchors the node's middle left as the point used when laying it out. */
+ static final Anchor WEST = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(west(node, x, w), centerY(node, y, h));
+ }
+ };
+
+ /** Anchors the node's top left as the point used when laying it out. */
+ static final Anchor NORTHWEST = new AbstractAnchor() {
+ /** {@inheritDoc} */
+ public void positionNode(final PNode node, final double x, final double y, final double w, final double h) {
+ node.setOffset(west(node, x, w), north(node, y, h));
+ }
+ };
+
+ static abstract class AbstractAnchor implements Anchor {
+ /**
+ * Returns the x at which the given node would need to be placed so
+ * that its center was in the middle of the horizontal segment
+ * defined by x and width.
+ *
+ * @param node node which is being analyzed
+ * @param x x component of horizontal line segment
+ * @param width width of horizontal line segment
+ * @return x at which node would need to be placed so that its
+ * center matched the center of the line segment
+ */
+ protected static double centerX(final PNode node, final double x, final double width) {
+ return x + (width - node.getFullBoundsReference().getWidth()) / 2;
+ }
+
+ /**
+ * Returns the y at which the given node would need to be placed so
+ * that its center was in the middle of the vertical segment defined
+ * by y and h.
+ *
+ * @param node node which is being analyzed
+ * @param y y component of horizontal line segment
+ * @param height height of vertical line segment
+ * @return y at which node would need to be placed so that its
+ * center matched the center of the line segment
+ */
+ protected static double centerY(final PNode node, final double y, final double height) {
+ return y + (height - node.getFullBoundsReference().getHeight()) / 2;
+ }
+
+ /**
+ * Returns the y at which the given node would need to be placed so
+ * that its top was against the top of the vertical segment defined.
+ *
+ * @param node node which is being analyzed
+ * @param y y component of horizontal line segment
+ * @param height height of vertical line segment
+ * @return y at which node would need to be placed so that its top
+ * matched the start of the line segment (y)
+ */
+ protected static double north(final PNode node, final double y, final double height) {
+ return y;
+ }
+
+ /**
+ * Returns the y at which the given node would need to be placed so
+ * that its bottom was against the bottom of the vertical range
+ * defined.
+ *
+ * @param node node which is being analyzed
+ * @param y y component of vertical range
+ * @param height height of vertical range
+ * @return y at which node would need to be placed so that its
+ * bottom matched the bottom of the range
+ */
+ protected static double south(final PNode node, final double y, final double height) {
+ return y + height - node.getFullBoundsReference().getHeight();
+ }
+
+ /**
+ * Returns the x at which the given node would need to be placed so
+ * that its right side was against the right side of the horizontal
+ * range defined.
+ *
+ * @param node node which is being analyzed
+ * @param x x component of horizontal range
+ * @param width width of horizontal range
+ * @return x at which node would need to be placed so that its right
+ * side touched the right side of the range defined.
+ */
+ protected static double east(final PNode node, final double x, final double width) {
+ return x + width - node.getFullBoundsReference().getWidth();
+ }
+
+ /**
+ * Returns the x at which the given node would need to be placed so
+ * that its left side was against the left side of the horizontal
+ * range defined.
+ *
+ * @param node node which is being analyzed
+ * @param x x component of horizontal range
+ * @param width width of horizontal range
+ * @return x at which node would need to be placed so that its left
+ * side touched the left side of the range defined (x)
+ */
+ protected static double west(final PNode node, final double x, final double width) {
+ return x;
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolox/swing/package.html b/src/main/java/edu/umd/cs/piccolox/swing/package.html
new file mode 100644
index 0000000..e78290c
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/swing/package.html
@@ -0,0 +1,34 @@
+
+
+
+This package supports using Piccolo with JFC/Swing.
+ + diff --git a/src/main/java/edu/umd/cs/piccolox/util/LineShape.java b/src/main/java/edu/umd/cs/piccolox/util/LineShape.java new file mode 100644 index 0000000..45e1b3f --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/util/LineShape.java @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.util; + +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +/** + * A shape that can be used to represent hand drawn lines. + */ +public class LineShape implements Shape, MutablePoints { + private MutablePoints points; + private final Rectangle2D bounds = new Rectangle2D.Double(); + + /** + * Constructs a LineShape from a list of mutable points. + * + * @param points points to use when constructing LineShape + */ + public LineShape(final MutablePoints points) { + setPoints(points); + } + + /** + * Changes the LineShape so that it's composed of the given points. + * + * @param points new Points to use as this shape's path + */ + public void setPoints(final MutablePoints points) { + if (points == null) { + this.points = new XYArray(); + } + else { + this.points = points; + } + } + + /** + * Returns the number points in this LineShape. + * + * @return # of points in this line shape + */ + public int getPointCount() { + return points.getPointCount(); + } + + /** + * Returns the x component of the point at the given index. + * + * @param pointIndex index of desired point + * + * @return x component of indexed point + */ + public double getX(final int pointIndex) { + return points.getX(pointIndex); + } + + /** + * Returns the y component of the point at the given index. + * + * @param pointIndex index of desired point + * + * @return y component of indexed point + */ + public double getY(final int pointIndex) { + return points.getY(pointIndex); + } + + /** + * Copies the point at the given index into the destination point. + * + * @param pointIndex the index of the desired point + * @param destinationPoint the point into which to load the values, or null + * if a new point is desired + * + * @return destinationPoint or new one if null was provided + */ + public Point2D getPoint(final int pointIndex, final Point2D destinationPoint) { + return points.getPoint(pointIndex, destinationPoint); + } + + /** + * Computes the bounds of this LineShape and stores them in the provided + * rectangle. + * + * @param dst rectangle to populate with this LineShape's bounds + * @return the bounds + */ + public Rectangle2D getBounds(final Rectangle2D dst) { + points.getBounds(dst); + return dst; + } + + /** + * Recalculates the bounds of this LineShape. + */ + public void updateBounds() { + bounds.setRect(0.0d, 0.0d, 0.0d, 0.0d); + points.getBounds(bounds); + } + + /** + * Sets the coordinate of the point at the given index. + * + * @param pointIndex index of the point to change + * @param x x component to assign to the point + * @param y y component to assign to the point + */ + public void setPoint(final int pointIndex, final double x, final double y) { + points.setPoint(pointIndex, x, y); + updateBounds(); + } + + /** + * Adds a point with the given coordinates at the desired index. + * + * @param pointIndex Index at which to add the point + * @param x x component of the new point + * @param y y component of the new point + */ + public void addPoint(final int pointIndex, final double x, final double y) { + points.addPoint(pointIndex, x, y); + updateBounds(); + } + + /** + * Removes n points from the LineShape starting at the provided index. + * + * @param pointIndex Starting index from which points are being removed + * @param num The number of sequential points to remove + */ + public void removePoints(final int pointIndex, final int num) { + points.removePoints(pointIndex, num); + updateBounds(); + } + + /** + * Applies the given transform to all points in this LineShape. + * + * @param transform Transform to apply + */ + public void transformPoints(final AffineTransform transform) { + final XYArray newPoints = new XYArray(points.getPointCount()); + newPoints.appendPoints(points); + newPoints.transformPoints(transform); + points = newPoints; + } + + /** + * Returns the current points of this LineShape as a simple Rectangle. + * + * @return bounds of this LineShape + */ + public Rectangle getBounds() { + return new Rectangle((int) bounds.getX(), (int) bounds.getY(), (int) bounds.getWidth(), (int) bounds + .getHeight()); + } + + /** + * Returns the current bounds in Rectangle2D format. + * + * @return bounds of LineShape as a Rectangle2D + */ + public Rectangle2D getBounds2D() { + return bounds; + } + + /** + * Returns whether the given coordinates are on the line defined by (x1,y1) + * and (x2,y2) within the given distance. + * + * @param x x component of point being tested + * @param y y component of point being tested + * @param x1 x component of start point of line segment + * @param y1 y component of start point of line segment + * @param x2 x component of end point of line segment + * @param y2 y component of end point of line segment + * @param min whether the point should be constrained to "after" the start + * of the segment + * @param max whether the point should be constrained to "before" the end of + * the segment + * @param distance distance from line acceptable as "touching" + * @return whether the point (x,y) is near enough to the given line + */ + public static boolean contains(final double x, final double y, final double x1, final double y1, final double x2, + final double y2, final boolean min, final boolean max, final double distance) { + double dx = x2 - x1; + double dy = y2 - y1; + + // If line is a point then bail out + if (dx == 0 && dy == 0) { + return false; + } + + final double dx2 = dx * dx; + final double dy2 = dy * dy; + + // distance along segment as a ratio or the (x1,y1)->(x2,y2) vector + final double p; + if (dx != 0) { + p = ((x - x1) / dx + dy * (y - y1) / dx2) / (1 + dy2 / dx2); + } + else { + p = ((y - y1) / dy + dx * (x - x1) / dy2) / (1 + dx2 / dy2); + } + + // Point is not "beside" the segment and it's been disallowed, bail. + if (min && p < 0 || max && p > 1.0) { + return false; + } + + dx = p * dx + x1 - x; + dy = p * dy + y1 - y; + + final double len = dx * dx + dy * dy; + return len < distance; + } + + /** + * Returns true if the given coordinates are within d units from any segment + * of the LineShape. + * + * @param x x component of point being tested + * @param y y component of point being tested + * @param d acceptable distance + * @return true if point is close enough to the LineShape + */ + public boolean contains(final double x, final double y, final double d) { + double x1, y1, x2, y2; + if (points.getPointCount() == 0) { + return false; + } + x2 = points.getX(0); + y2 = points.getX(0); + for (int i = 0; i < points.getPointCount(); i++) { + x1 = x2; + y1 = y2; + x2 = points.getX(i); + y2 = points.getX(i); + if (contains(x, y, x1, y1, x2, y2, false, false, d)) { + return true; + } + } + return false; + } + + /** + * Returns true if point is within 2 pixels of any line segment of this + * LineShape. + * + * @param x x component of point being tested + * @param y y component of point being tested + * @return true if point is within 2 pixels of any of this LineShape's + * segments + */ + public boolean contains(final double x, final double y) { + return contains(x, y, 2.0d); + } + + /** + * Returns true if point is within 2 pixels of any line segment of this + * LineShape. + * + * @param p point being tested + * @return true if point is within 2 pixels of any of this LineShape's + * segments + */ + public boolean contains(final Point2D p) { + return contains(p.getX(), p.getY()); + } + + /** + * Returns true if the two segments defined by (x1,y1)->(x2,y2) and + * (x3,y3)->(x4,y4) intersect. Optional fields allow for consideration of + * extending the segments to infinity at either end. + * + * @param x1 segment 1's start x component + * @param y1 segment 1's start y component + * @param x2 segment 1's end x component + * @param y2 segment 1's end y component + * @param x3 segment 2's start x component + * @param y3 segment 2's start y component + * @param x4 segment 2's end x component + * @param y4 segment 2's end y component + * @param min1 whether the second segment is acceptable if it passes + * "before the start of the first segment" + * @param max1 whether the second segment is acceptable if it passes + * "after the end of the first segment" + * @param min2 whether the first segment is acceptable if it passes + * "before the start of the second segment" + * @param max2 whether the first segment is acceptable if it passes + * "after the start of the second segment" + * @return true if line segments intersect + */ + public static boolean intersects(final double x1, final double y1, final double x2, final double y2, + final double x3, final double y3, final double x4, final double y4, final boolean min1, final boolean max1, + final boolean min2, final boolean max2) { + final double dx1 = x2 - x1, dy1 = y2 - y1, dx2 = x4 - x3, dy2 = y4 - y3; + double d, p2, p1; + + if (dy1 != 0.0) { + d = dx1 / dy1; + p2 = (x3 - x1 + d * (y1 - y3)) / (d * dy2 - dx2); + p1 = (dy2 * p2 + y3 - y1) / dy1; + } + else if (dy2 != 0.0) { + d = dx2 / dy2; + p1 = (x1 - x3 + d * (y3 - y1)) / (d * dy1 - dx1); + p2 = (dy1 * p1 + y1 - y3) / dy2; + } + else if (dx1 != 0.0) { + d = dy1 / dx1; + p2 = (y3 - y1 + d * (x1 - x3)) / (d * dx2 - dy2); + p1 = (dx2 * p2 + x3 - x1) / dx1; + } + else if (dx2 != 0.0) { + d = dy2 / dx2; + p1 = (y1 - y3 + d * (x3 - x1)) / (d * dx1 - dy1); + p2 = (dx1 * p1 + x1 - x3) / dx2; + } + else { + return false; + } + return (!min1 || p1 >= 0.0) && (!max1 || p1 <= 1.0) && (!min2 || p2 >= 0.0) && (!max2 || p2 <= 1.0); + } + + /** + * Returns true if any segment crosses an edge of the rectangle. + * + * @param x left of rectangle to be tested + * @param y top of rectangle to be tested + * @param w width of rectangle to be tested + * @param h height of rectangle to be tested + * + * @return true if rectangle intersects + */ + public boolean intersects(final double x, final double y, final double w, final double h) { + double x1, y1, x2, y2; + if (points.getPointCount() == 0) { + return false; + } + x2 = points.getX(0); + y2 = points.getY(0); + for (int i = 0; i < points.getPointCount(); i++) { + x1 = x2; + y1 = y2; + x2 = points.getX(i); + y2 = points.getY(i); + if (intersects(x, y, x + w, y, x1, y1, x2, y2, true, true, true, true) + || intersects(x + w, y, x + w, y + h, x1, y1, x2, y2, true, true, true, true) + || intersects(x + w, y + h, x, y + h, x1, y1, x2, y2, true, true, true, true) + || intersects(x, y + h, x, y, x1, y1, x2, y2, true, true, true, true)) { + return true; + } + } + return false; + } + + /** + * Returns true if any segment crosses an edge of the rectangle. + * + * @param r rectangle to be tested + * @return true if rectangle intersects + */ + public boolean intersects(final Rectangle2D r) { + return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + /** + * Whether the LineShape contains the rectangle defined. + * + * @param x left of defined rectangle + * @param y top of defined rectangle + * @param width width of defined rectangle + * @param height height of defined rectangle + * @return true if rectangle is contained + */ + public boolean contains(final double x, final double y, final double width, final double height) { + return contains(x, y) && contains(x + width, y) && contains(x, y + height) && contains(x + width, y + height); + } + + /** + * Whether the LineShape contains the rectangle. + * + * @param r rectangle being tested + * @return true if rectangle is contained + */ + public boolean contains(final Rectangle2D r) { + return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + /** + * Returns an iterator that can be used to iterate of the segments of this + * LineShape. Optionally applying the given transform before returning it. + * + * @param at optional transform to apply to segment before returning it. May + * be null + * @return iterator for iterating segments of this LineShape + */ + public PathIterator getPathIterator(final AffineTransform at) { + return new LinePathIterator(points, at); + } + + /** + * Returns an iterator that can be used to iterate of the segments of this + * LineShape. Optionally applying the given transform before returning it. + * + * @param at optional transform to apply to segment before returning it. May + * be null + * @param flatness ignored completely + * @return iterator for iterating segments of this LineShape + */ + public PathIterator getPathIterator(final AffineTransform at, final double flatness) { + return new LinePathIterator(points, at); + } + + private static class LinePathIterator implements PathIterator { + + private final Points points; + private final AffineTransform trans; + private int i = 0; + + /** + * Constructs a LinePathIterator for the given points and with an + * optional transform. + * + * @param points points to be iterated + * @param trans optional iterator to apply to paths before returning + * them + */ + public LinePathIterator(final Points points, final AffineTransform trans) { + this.points = points; + this.trans = trans; + } + + /** + * Returns the winding rule being applied when selecting next paths. + * + * @return GeneralPath.WIND_EVEN_ODD since that's the only policy + * supported + */ + public int getWindingRule() { + return GeneralPath.WIND_EVEN_ODD; + } + + /** + * Returns true if there are no more paths to iterate over. + * + * @return true if iteration is done + */ + public boolean isDone() { + return i >= points.getPointCount(); + } + + /** + * Moves to the next path. + */ + public void next() { + i++; + } + + private final Point2D tempPoint = new Point2D.Double(); + + private void currentSegment() { + tempPoint.setLocation(points.getX(i), points.getY(i)); + if (trans != null) { + trans.transform(tempPoint, tempPoint); + } + } + + /** + * Populates the given array with the current segment and returns the + * type of segment. + * + * @param coords array to be populated + * + * @return type of segment SEG_MOVETO or SEG_LINETO + */ + public int currentSegment(final float[] coords) { + currentSegment(); + coords[0] = (float) tempPoint.getX(); + coords[1] = (float) tempPoint.getY(); + if (i == 0) { + return PathIterator.SEG_MOVETO; + } + else { + return PathIterator.SEG_LINETO; + } + } + + /** + * Populates the given array with the current segment and returns the + * type of segment. + * + * @param coords array to be populated + * + * @return type of segment SEG_MOVETO or SEG_LINETO + */ + public int currentSegment(final double[] coords) { + currentSegment(); + coords[0] = tempPoint.getX(); + coords[1] = tempPoint.getY(); + if (i == 0) { + return PathIterator.SEG_MOVETO; + } + else { + return PathIterator.SEG_LINETO; + } + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/util/MutablePoints.java b/src/main/java/edu/umd/cs/piccolox/util/MutablePoints.java new file mode 100644 index 0000000..e87e371 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/util/MutablePoints.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.util; + +import java.awt.geom.AffineTransform; + +/** + * Minimal interface that a changeable sequence of points must provide. + */ +public interface MutablePoints extends Points { + /** + * Sets the coordinates for the point at the given index. + * + * @param i index of point + * @param x x component of the point's coordinates + * @param y y component of the point's coordinates + */ + void setPoint(int i, double x, double y); + + /** + * Inserts a point at the specified position. + * + * @param pos position at which to insert the point + * @param x x component of the point's coordinates + * @param y y component of the point's coordinates + */ + void addPoint(int pos, double x, double y); + + /** + * Removes a subsequence of points. + * + * @param pos position to start removing points + * @param num number of points to remove + */ + void removePoints(int pos, int num); + + /** + * Modifies all points by applying the transform to them. + * + * @param t transformto apply to the points + */ + void transformPoints(AffineTransform t); +} diff --git a/src/main/java/edu/umd/cs/piccolox/util/PBoundsLocator.java b/src/main/java/edu/umd/cs/piccolox/util/PBoundsLocator.java new file mode 100644 index 0000000..0698540 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/util/PBoundsLocator.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.util; + +import java.awt.geom.Rectangle2D; + +import javax.swing.SwingConstants; + +import edu.umd.cs.piccolo.PNode; + +/** + * PBoundsLocator is a locator that locates points on the bounds of a + * node. + *+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public class PBoundsLocator extends PNodeLocator { + private static final long serialVersionUID = 1L; + private int side; + + /** + * Creates a locator for tracking the east side of the provided node. + * + * @param node node to track + * @return a new locator + */ + public static PBoundsLocator createEastLocator(final PNode node) { + return new PBoundsLocator(node, SwingConstants.EAST); + } + + /** + * Creates a locator for tracking the north east corner of the provided + * node. + * + * @param node node to track + * @return a new locator + */ + public static PBoundsLocator createNorthEastLocator(final PNode node) { + return new PBoundsLocator(node, SwingConstants.NORTH_EAST); + } + + /** + * Creates a locator for tracking the north west corner of the provided + * node. + * + * @param node node to track + * @return a new locator + */ + public static PBoundsLocator createNorthWestLocator(final PNode node) { + return new PBoundsLocator(node, SwingConstants.NORTH_WEST); + } + + /** + * Creates a locator for tracking the north side of the provided node. + * + * @param node node to track + * @return a new locator + */ + public static PBoundsLocator createNorthLocator(final PNode node) { + return new PBoundsLocator(node, SwingConstants.NORTH); + } + + /** + * Creates a locator for tracking the south side of the provided node. + * + * @param node node to track + * @return a new locator + */ + public static PBoundsLocator createSouthLocator(final PNode node) { + return new PBoundsLocator(node, SwingConstants.SOUTH); + } + + /** + * Creates a locator for tracking the west side of the provided node. + * + * @param node node to track + * @return a new locator + */ + public static PBoundsLocator createWestLocator(final PNode node) { + return new PBoundsLocator(node, SwingConstants.WEST); + } + + /** + * Creates a locator for tracking the south west corner of the provided + * node. + * + * @param node node to track + * @return a new locator + */ + public static PBoundsLocator createSouthWestLocator(final PNode node) { + return new PBoundsLocator(node, SwingConstants.SOUTH_WEST); + } + + /** + * Creates a locator for tracking the south east corner of the provided + * node. + * + * @param node node to track + * @return a new locator + */ + public static PBoundsLocator createSouthEastLocator(final PNode node) { + return new PBoundsLocator(node, SwingConstants.SOUTH_EAST); + } + + /** + * Constructs a locator for tracking the position on the node provided. + * + * @param node node to track + * @param aSide specified the position on the node to track + */ + public PBoundsLocator(final PNode node, final int aSide) { + super(node); + side = aSide; + } + + /** + * Returns the side of the node that's being tracked. + * + * @return tracked side + */ + public int getSide() { + return side; + } + + /** + * Sets the side to track on the node. + * + * @param side new side to track + */ + public void setSide(final int side) { + this.side = side; + } + + /** + * Maps the locator's side to its x position. + * + * @return x position on side this locator is tracking + */ + public double locateX() { + final Rectangle2D aBounds = node.getBoundsReference(); + + switch (side) { + case SwingConstants.NORTH_WEST: + case SwingConstants.SOUTH_WEST: + case SwingConstants.WEST: + return aBounds.getX(); + + case SwingConstants.NORTH_EAST: + case SwingConstants.SOUTH_EAST: + case SwingConstants.EAST: + return aBounds.getX() + aBounds.getWidth(); + + case SwingConstants.NORTH: + case SwingConstants.SOUTH: + return aBounds.getX() + aBounds.getWidth() / 2; + default: + return -1; + } + } + + /** + * Maps the locator's side to its y position. + * + * @return y position on side this locator is tracking + */ + public double locateY() { + final Rectangle2D aBounds = node.getBoundsReference(); + + switch (side) { + case SwingConstants.EAST: + case SwingConstants.WEST: + return aBounds.getY() + aBounds.getHeight() / 2; + + case SwingConstants.SOUTH: + case SwingConstants.SOUTH_WEST: + case SwingConstants.SOUTH_EAST: + return aBounds.getY() + aBounds.getHeight(); + + case SwingConstants.NORTH_WEST: + case SwingConstants.NORTH_EAST: + case SwingConstants.NORTH: + return aBounds.getY(); + default: + return -1; + } + } +} diff --git a/src/main/java/edu/umd/cs/piccolox/util/PFixedWidthStroke.java b/src/main/java/edu/umd/cs/piccolox/util/PFixedWidthStroke.java new file mode 100644 index 0000000..9318c65 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/util/PFixedWidthStroke.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.util; + +import java.awt.BasicStroke; +import java.awt.Stroke; +import java.io.ObjectStreamException; +import java.io.Serializable; + +/** + * PFixedWidthStroke is the same as {@link BasicStroke} except that + * PFixedWidthStroke has a fixed width on the screen so that even when the + * canvas view is zooming its width stays the same in canvas coordinates. + *
+ * {@link #createStrokedShape(Shape)} checks if the scale has changed since the + * last usage and if that's the case calls {@link #newStroke(float)} to get a + * new {@link Stroke} instance to delegate to. + *
+ * CAUTION! this implementation falls short for large scaling factors - + * the effective miterlimit might drop below 1.0 which isn't permitted by + * {@link BasicStroke} and therefore limited to a minimal 1.0 by this + * implementation. A more sophisticated implementation might use the approach + * mentioned at http://code.google.com/p/piccolo2d/issues/detail?id=49 + *
+ * CAUTION! after extreme scaling this implementation seems to change to
+ * internal state of the base stroke. Try PathExample with extreme zoom in and
+ * zoom back to the original scale. The pickable circles disappear. Strange!
+ *
+ * @see edu.umd.cs.piccolo.nodes.PPath
+ * @see BasicStroke
+ * @version 1.0
+ * @author Jesse Grosjean
+ */
+public class PFixedWidthStroke extends PSemanticStroke implements Serializable {
+
+ private static final float DEFAULT_MITER_LIMIT = 10.0f;
+
+ private static final BasicStroke DEFAULT_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE,
+ BasicStroke.JOIN_MITER, DEFAULT_MITER_LIMIT, null, 0.0f);
+
+ private static final long serialVersionUID = 1L;
+
+ // avoid repeated cloning:
+ private final transient float[] dash;
+
+ // avoid repeated instantiations:
+ private final transient float[] tmpDash;
+
+ /**
+ * Constructs a simple PFixedWidthStroke with the default stroke.
+ */
+ public PFixedWidthStroke() {
+ this(DEFAULT_STROKE);
+ }
+
+ /**
+ * Making this constructor public would break encapsulation. Users don't
+ * need to know that they are dealing with an adapter to an underlying
+ * stroke.
+ *
+ * @param stroke stroke being used by this PFixedWithStroke
+ */
+ private PFixedWidthStroke(final BasicStroke stroke) {
+ super(stroke);
+ dash = stroke.getDashArray();
+ if (dash == null) {
+ tmpDash = null;
+ }
+ else {
+ tmpDash = new float[dash.length];
+ }
+ }
+
+ /**
+ * Constructs a simple PFixedWidthStroke with the width provided.
+ *
+ * @param width desired width of the stroke
+ */
+ public PFixedWidthStroke(final float width) {
+ this(width, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, DEFAULT_MITER_LIMIT, null, 0.0f);
+ }
+
+ /**
+ * Constructs a PFixedWidthStroke with the stroke properties provided.
+ *
+ * @param width width of stroke
+ * @param cap cap to use in stroke
+ * @param join join to use in stroke
+ */
+ public PFixedWidthStroke(final float width, final int cap, final int join) {
+ this(width, cap, join, DEFAULT_MITER_LIMIT, null, 0.0f);
+ }
+
+ /**
+ * Constructs a PFixedWidthStroke with the stroke properties provided.
+ *
+ * @param width width of stroke
+ * @param cap cap to use in stroke
+ * @param join join to use in stroke
+ * @param miterlimit miter limit of stroke
+ */
+ public PFixedWidthStroke(final float width, final int cap, final int join, final float miterlimit) {
+ this(width, cap, join, miterlimit, null, 0.0f);
+ }
+
+ /**
+ * Constructs a PFixedWidthStroke with the stroke properties provided.
+ *
+ * @param width width of stroke
+ * @param cap cap to use in stroke
+ * @param join join to use in stroke
+ * @param miterlimit miter limit of stroke
+ * @param dash array of dash lengths
+ * @param dashPhase phase to use when rendering dashes
+ */
+ public PFixedWidthStroke(final float width, final int cap, final int join, final float miterlimit,
+ final float[] dash, final float dashPhase) {
+ this(new BasicStroke(width, cap, join, miterlimit, dash, dashPhase));
+ }
+
+ /**
+ * Throws an exception since PFixedWidthStrokes are not serializable.
+ *
+ * @return never returns anything
+ */
+ public Object clone() {
+ throw new UnsupportedOperationException("Not implemented.");
+ }
+
+ /**
+ * Returns the array representing the lengths of the dash segments.
+ * Alternate entries in the array represent the user space lengths of the
+ * opaque and transparent segments of the dashes. As the pen moves along the
+ * outline of the Shape to be stroked, the user space distance that the pen
+ * travels is accumulated. The distance value is used to index into the dash
+ * array. The pen is opaque when its current cumulative distance maps to an
+ * even element of the dash array and transparent otherwise.
+ *
+ * @return the dash array
+ */
+ public float[] getDashArray() {
+ return ((BasicStroke) stroke).getDashArray();
+ }
+
+ /**
+ * Returns the current dash phase. The dash phase is a distance specified in
+ * user coordinates that represents an offset into the dashing pattern. In
+ * other words, the dash phase defines the point in the dashing pattern that
+ * will correspond to the beginning of the stroke.
+ *
+ * @return the dash phase as a float value.
+ */
+ public float getDashPhase() {
+ return ((BasicStroke) stroke).getDashPhase();
+ }
+
+ /**
+ * Returns the end cap style.
+ *
+ * @return the end cap style of this BasicStroke as one of the static int values that define possible end cap styles.
+ */
+ public int getEndCap() {
+ return ((BasicStroke) stroke).getEndCap();
+ }
+
+ /**
+ * Returns the line join style.
+ *
+ * @return the line join style of the PFixedWidthStroke
as one
+ * of the static int
values that define possible line
+ * join styles.
+ */
+ public int getLineJoin() {
+ return ((BasicStroke) stroke).getLineJoin();
+ }
+
+ /**
+ * Returns the line width. Line width is represented in user space, which is
+ * the default-coordinate space used by Java 2D. See the Graphics2D class
+ * comments for more information on the user space coordinate system.
+ *
+ * @return the line width of this BasicStroke.
+ */
+ public float getLineWidth() {
+ return ((BasicStroke) stroke).getLineWidth();
+ }
+
+ /**
+ * Returns the miter limit of this node.
+ *
+ * @return the limit of miter joins of the PFixedWidthStroke
+ */
+ public float getMiterLimit() {
+ return ((BasicStroke) stroke).getMiterLimit();
+ }
+
+ /**
+ * Returns a stroke equivalent to this one, but scaled by the scale
+ * provided.
+ *
+ * @param activeScale scale to apply to the new stoke
+ * @return scaled stroke
+ */
+ protected Stroke newStroke(final float activeScale) {
+ if (tmpDash != null) {
+ for (int i = dash.length - 1; i >= 0; i--) {
+ tmpDash[i] = dash[i] / activeScale;
+ }
+ }
+ final float ml = getMiterLimit() / activeScale;
+ final float sanitizedMiterLimit;
+ if (ml < 1.0f) {
+ sanitizedMiterLimit = 1f;
+ }
+ else {
+ sanitizedMiterLimit = ml;
+ }
+
+ return new BasicStroke(getLineWidth() / activeScale, getEndCap(), getLineJoin(), sanitizedMiterLimit, tmpDash,
+ getDashPhase() / activeScale);
+ }
+
+ /**
+ * Is it really necessary to implement {@link Serializable}?
+ *
+ * @throws ObjectStreamException doesn't actually throw this at all, why's
+ * this here?
+ * @return the resolved stroke
+ */
+ protected Object readResolve() throws ObjectStreamException {
+ return new PFixedWidthStroke((BasicStroke) stroke);
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/util/PLocator.java b/src/main/java/edu/umd/cs/piccolox/util/PLocator.java
new file mode 100644
index 0000000..fbe80a0
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/util/PLocator.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.util;
+
+import java.awt.geom.Point2D;
+import java.io.Serializable;
+
+/**
+ * PLocator provides an abstraction for locating points. Subclasses such
+ * as PNodeLocator and PBoundsLocator specialize this behavior by locating
+ * points on nodes, or on the bounds of nodes.
+ *
+ * + * @version 1.0 + * @author Jesse Grosjean + */ +public abstract class PLocator implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Default constructor provided for subclasses. Does nothing by itself. + */ + public PLocator() { + } + + /** + * Locates the point this locator is responsible for finding, and stores it + * in dstPoints. Should dstPoints be null, it will create a new point and + * return it. + * + * @param dstPoint output parameter to store the located point + * @return the located point + */ + public Point2D locatePoint(final Point2D dstPoint) { + Point2D result; + if (dstPoint == null) { + result = new Point2D.Double(); + } + else { + result = dstPoint; + } + result.setLocation(locateX(), locateY()); + return result; + } + + /** + * Locates the X component of the position this locator finds. + * + * @return x component of located point + */ + public abstract double locateX(); + + /** + * Locates the Y component of the position this locator finds. + * + * @return y component of located point + */ + public abstract double locateY(); +} diff --git a/src/main/java/edu/umd/cs/piccolox/util/PNodeLocator.java b/src/main/java/edu/umd/cs/piccolox/util/PNodeLocator.java new file mode 100644 index 0000000..fb327a8 --- /dev/null +++ b/src/main/java/edu/umd/cs/piccolox/util/PNodeLocator.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package edu.umd.cs.piccolox.util; + +import edu.umd.cs.piccolo.PNode; + +/** + * PNodeLocator provides an abstraction for locating points on a node. + * Points are located in the local corrdinate system of the node. The default + * behavior is to locate the center point of the nodes bounds. The node where + * the point is located is stored internal to this locator (as an instance + * varriable). If you want to use the same locator to locate center points on + * many different nodes you will need to call setNode() before asking for each + * location. + *
+ *
+ * @version 1.0
+ * @author Jesse Grosjean
+ */
+public class PNodeLocator extends PLocator {
+ private static final long serialVersionUID = 1L;
+
+ /** Node being located by this locator. */
+ protected PNode node;
+
+ /**
+ * Constructs a locator responsible for locating the given node.
+ *
+ * @param node node to be located
+ */
+ public PNodeLocator(final PNode node) {
+ setNode(node);
+ }
+
+ /**
+ * Returns the node being located by this locator.
+ *
+ * @return node being located by this locator
+ */
+ public PNode getNode() {
+ return node;
+ }
+
+ /**
+ * Changes the node being located by this locator.
+ *
+ * @param node new node to have this locator locate.
+ */
+ public void setNode(final PNode node) {
+ this.node = node;
+ }
+
+ /**
+ * Locates the left of the target node's bounds.
+ *
+ * @return left of target node's bounds
+ */
+ public double locateX() {
+ return node.getBoundsReference().getCenterX();
+ }
+
+ /**
+ * Locates the top of the target node's bounds.
+ *
+ * @return top of target node's bounds
+ */
+ public double locateY() {
+ return node.getBoundsReference().getCenterY();
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/util/POcclusionDetection.java b/src/main/java/edu/umd/cs/piccolox/util/POcclusionDetection.java
new file mode 100644
index 0000000..88c7090
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/util/POcclusionDetection.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.util;
+
+import edu.umd.cs.piccolo.PNode;
+import edu.umd.cs.piccolo.util.PBounds;
+import edu.umd.cs.piccolo.util.PPickPath;
+
+/**
+ * Experimental class for detecting occlusions.
+ *
+ * @author Jesse Grosjean
+ */
+public class POcclusionDetection {
+
+ /**
+ * Traverse from the bottom right of the scene graph (top visible node) up
+ * the tree determining which parent nodes are occluded by their children
+ * nodes. Note that this is only detecting a subset of occlusions (parent,
+ * child), others such as overlapping siblings or cousins are not detected.
+ *
+ * @param n node from which to detect occlusions
+ * @param parentBounds bounds of parent node
+ */
+ public void detectOccusions(final PNode n, final PBounds parentBounds) {
+ detectOcclusions(n, new PPickPath(null, parentBounds));
+ }
+
+ /**
+ * Traverse the pick path determining which parent nodes are occluded by
+ * their children nodes. Note that this is only detecting a subset of
+ * occlusions (parent, child), others such as overlapping siblings or
+ * cousins are not detected.
+ *
+ * @param node node from which to detect occlusions
+ * @param pickPath Pick Path to traverse
+ */
+ public void detectOcclusions(final PNode node, final PPickPath pickPath) {
+ if (!node.fullIntersects(pickPath.getPickBounds())) {
+ return;
+ }
+
+ pickPath.pushTransform(node.getTransformReference(false));
+
+ final int count = node.getChildrenCount();
+ for (int i = count - 1; i >= 0; i--) {
+ final PNode each = node.getChild(i);
+ if (node.getOccluded()) {
+ // if n has been occluded by a previous descendant then
+ // this child must also be occluded
+ each.setOccluded(true);
+ }
+ else {
+ // see if child each occludes n
+ detectOcclusions(each, pickPath);
+ }
+ }
+
+ if (nodeOccludesParents(node, pickPath)) {
+ final PNode parent = node.getParent();
+ while (parent != null && !parent.getOccluded()) {
+ parent.setOccluded(true);
+ }
+ }
+
+ pickPath.popTransform(node.getTransformReference(false));
+ }
+
+ /**
+ * Calculate whether node occludes its parents.
+ *
+ * @param n node to test
+ * @param pickPath pickpath identifying the parents of the node
+ * @return true if parents are occluded by the node
+ */
+ private boolean nodeOccludesParents(final PNode n, final PPickPath pickPath) {
+ return !n.getOccluded() && n.intersects(pickPath.getPickBounds()) && n.isOpaque(pickPath.getPickBounds());
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/util/PSemanticStroke.java b/src/main/java/edu/umd/cs/piccolox/util/PSemanticStroke.java
new file mode 100644
index 0000000..96ca42d
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/util/PSemanticStroke.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.util;
+
+import java.awt.Shape;
+import java.awt.Stroke;
+
+import edu.umd.cs.piccolo.util.PDebug;
+import edu.umd.cs.piccolo.util.PPaintContext;
+import edu.umd.cs.piccolo.util.PPickPath;
+
+/**
+ *
+ * @see edu.umd.cs.piccolo.nodes.PPath
+ * @see Stroke
+ * @version 1.3
+ * @author Marcus Rohrmoser
+ */
+abstract class PSemanticStroke implements Stroke {
+ protected static final double THRESHOLD = 1e-6;
+
+ private transient float recentScale;
+ private transient Stroke recentStroke;
+ protected final Stroke stroke;
+
+ protected PSemanticStroke(final Stroke stroke) {
+ this.stroke = stroke;
+ recentStroke = stroke;
+ recentScale = 1.0F;
+ }
+
+ /**
+ * Ask {@link #getActiveScale()}, call {@link #newStroke(float)} if
+ * necessary and delegate to {@link Stroke#createStrokedShape(Shape)}.
+ *
+ * @param s
+ */
+ public Shape createStrokedShape(final Shape s) {
+ final float currentScale = getActiveScale();
+ if (Math.abs(currentScale - recentScale) > THRESHOLD) {
+ recentScale = currentScale;
+ recentStroke = newStroke(recentScale);
+ }
+ return recentStroke.createStrokedShape(s);
+ }
+
+ /**
+ * Returns true if this stroke is equivalent to the object provided.
+ *
+ * @param obj Object being tested
+ * @return true if object is equivalent
+ */
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final PSemanticStroke other = (PSemanticStroke) obj;
+ if (stroke == null) {
+ if (other.stroke != null) {
+ return false;
+ }
+ }
+ else if (!stroke.equals(other.stroke)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Detect the current scale. Made protected to enable custom
+ * re-implementations.
+ */
+ protected float getActiveScale() {
+ // FIXME Honestly I don't understand this distinction - shouldn't it
+ // always be PPaintContext.CURRENT_PAINT_CONTEXT regardless of the
+ // debugging flag?
+ if (PDebug.getProcessingOutput()) {
+ if (PPaintContext.CURRENT_PAINT_CONTEXT != null) {
+ return (float) PPaintContext.CURRENT_PAINT_CONTEXT.getScale();
+ }
+ }
+ else {
+ if (PPickPath.CURRENT_PICK_PATH != null) {
+ return (float) PPickPath.CURRENT_PICK_PATH.getScale();
+ }
+ }
+ return 1.0f;
+ }
+
+ public int hashCode() {
+ final int prime = 31;
+ int result = prime;
+
+ if (stroke != null) {
+ result += stroke.hashCode();
+ }
+
+ return result;
+ }
+
+ /**
+ * Factory to create a new internal stroke delegate. Made protected to
+ * enable custom re-implementations.
+ */
+ protected abstract Stroke newStroke(final float activeScale);
+
+ public String toString() {
+ return stroke.toString();
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/util/Points.java b/src/main/java/edu/umd/cs/piccolox/util/Points.java
new file mode 100644
index 0000000..83f3e1e
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/util/Points.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.util;
+
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Interface for a sequence of points.
+ */
+public interface Points {
+ /**
+ * Returns the number of points in the sequence.
+ *
+ * @return number of points in the sequence
+ */
+ int getPointCount();
+
+ /**
+ * Returns the x component of the point at the given index.
+ *
+ * @param i index of desired point
+ *
+ * @return x component of point
+ */
+ double getX(int i);
+
+ /**
+ * Returns the y component of the point at the given index.
+ *
+ * @param i index of desired point
+ *
+ * @return y component of point
+ */
+ double getY(int i);
+
+ /**
+ * Returns a point representation of the coordinates at the given index.
+ *
+ * @param i index of desired point
+ * @param dst output parameter into which the point's details will be
+ * populated, if null a new one will be created.
+ *
+ * @return a point representation of the coordinates at the given index
+ */
+ Point2D getPoint(int i, Point2D dst);
+
+ /**
+ * Returns the bounds of all the points taken as a whole.
+ *
+ * @param dst output parameter to store bounds into, if null a new rectangle
+ * will be created
+ * @return rectangle containing the bounds
+ */
+ Rectangle2D getBounds(Rectangle2D dst);
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/util/ShadowUtils.java b/src/main/java/edu/umd/cs/piccolox/util/ShadowUtils.java
new file mode 100644
index 0000000..ed16beb
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/util/ShadowUtils.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.util;
+
+import java.awt.AlphaComposite;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Paint;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.ConvolveOp;
+import java.awt.image.Kernel;
+
+/**
+ * Static utility methods for creating shadows.
+ *
+ * @since 1.3
+ */
+public final class ShadowUtils {
+
+ private static final int BLUR_BOUNDS_AFFORDANCE = 4;
+
+ /**
+ * Private no-arg constructor.
+ */
+ private ShadowUtils() {
+ // empty
+ }
+
+ /**
+ * Create and return a new buffered image containing a shadow of the
+ * specified source image using the specifed shadow paint and gaussian blur
+ * radius. The dimensions of the returned image will be
+ * src.getWidth() + 4 * blurRadius
x
+ * src.getHeight() + 4 * blurRadius
to account for blurring
+ * beyond the bounds of the source image. Thus the source image will appear
+ * to be be offset by (2 * blurRadius
,
+ * 2 * blurRadius
) in the returned image.
+ *
+ * @param src source image, must not be null
+ * @param shadowPaint shadow paint
+ * @param blurRadius gaussian blur radius, must be > 0
+ * @return a new buffered image containing a shadow of the specified source
+ * image using the specifed shadow paint and gaussian blur radius
+ */
+ public static BufferedImage createShadow(final Image src, final Paint shadowPaint, final int blurRadius) {
+ if (src == null) {
+ throw new IllegalArgumentException("src image must not be null");
+ }
+ if (blurRadius < 1) {
+ throw new IllegalArgumentException("blur radius must be greater than zero, was " + blurRadius);
+ }
+ int w = src.getWidth(null) + (BLUR_BOUNDS_AFFORDANCE * blurRadius);
+ int h = src.getHeight(null) + (BLUR_BOUNDS_AFFORDANCE * blurRadius);
+
+ // paint src image into mask
+ BufferedImage mask = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = mask.createGraphics();
+ g.drawImage(src, 2 * blurRadius, 2 * blurRadius, null);
+
+ // composite mask with shadow paint
+ g.setComposite(AlphaComposite.SrcIn);
+ g.setPaint(shadowPaint);
+ g.fillRect(0, 0, w, h);
+ g.dispose();
+
+ // apply convolve op for blur
+ ConvolveOp convolveOp = new ConvolveOp(new GaussianKernel(blurRadius));
+ BufferedImage shadow = convolveOp.filter(mask, null);
+ return shadow;
+ }
+
+ /**
+ * Gaussian kernel.
+ */
+ private static class GaussianKernel extends Kernel {
+
+ /**
+ * Create a new gaussian kernel with the specified blur radius.
+ *
+ * @param blurRadius blur radius
+ */
+ GaussianKernel(final int blurRadius) {
+ super((2 * blurRadius) + 1, (2 * blurRadius) + 1, createKernel(blurRadius));
+ }
+
+ /**
+ * Create an array of floats representing a gaussian kernel with the
+ * specified radius.
+ *
+ * @param r radius
+ * @return an array of floats representing a gaussian kernel with the
+ * specified radius
+ */
+ private static float[] createKernel(final int r) {
+ int w = (2 * r) + 1;
+ float[] kernel = new float[w * w];
+ double m = 2.0d * Math.pow((r / 3.0d), 2);
+ double n = Math.PI * m;
+
+ double sum = 0.0d;
+ for (int x = 0; x < w; x++) {
+ int xr2 = (x - r) * (x - r);
+ for (int y = 0; y < w; y++) {
+ int yr2 = (y - r) * (y - r);
+ kernel[x * w + y] = (float) (Math.pow(Math.E, -(yr2 + xr2) / m) / n);
+ sum += kernel[x * w + y];
+ }
+ }
+
+ for (int i = kernel.length - 1; i >= 0; i--) {
+ kernel[i] /= sum;
+ }
+ return kernel;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/umd/cs/piccolox/util/XYArray.java b/src/main/java/edu/umd/cs/piccolox/util/XYArray.java
new file mode 100644
index 0000000..2588665
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/util/XYArray.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
+ * Copyright (c) 1998-2008, University of Maryland
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
+ * and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+ * and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
+ * contributors may be used to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package edu.umd.cs.piccolox.util;
+
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Represents a sequence as points that's internally stored as a single array of
+ * point components.
+ */
+public class XYArray implements MutablePoints, Cloneable {
+ /** The coordinates of the points, specifically 2x the number of points. */
+ private double[] points = null;
+
+ /** the number of valid x, y pairs. */
+ private int numPoints = 0;
+
+ /**
+ * Constructs an XYArray wrapping the given points.
+ *
+ * @param points array of coordinates defining the points
+ */
+ public XYArray(final double[] points) {
+ initPoints(points, points.length / 2);
+ }
+
+ /**
+ * Constructs an XYArray of the given size.
+ *
+ * @param n number of points XYArray should contain
+ */
+ public XYArray(final int n) {
+ initPoints(null, n);
+ }
+
+ /**
+ * Constructs an empty XYArray.
+ */
+ public XYArray() {
+ this(0);
+ }
+
+ /**
+ * Returns the number of points this XYArray represents.
+ *
+ * @return number of points
+ */
+ public int getPointCount() {
+ return numPoints;
+ }
+
+ /**
+ * Converts negative indexes to positive ones by adding numPoints to it.
+ *
+ * @param i index to be normalized
+ * @return normalized index
+ */
+ private int normalize(final int i) {
+ if (i >= numPoints) {
+ throw new IllegalArgumentException("The point index " + i + " is not below " + numPoints);
+ }
+
+ if (i < 0) {
+ return numPoints + i;
+ }
+ else {
+ return i;
+ }
+ }
+
+ /**
+ * Returns the x component of the point at the given index.
+ *
+ * @param i index of point
+ * @return x component of point at given index
+ */
+ public double getX(final int i) {
+ return points[normalize(i) * 2];
+ }
+
+ /**
+ * Returns the y component of the point at the given index.
+ *
+ * @param i index of point
+ * @return y component of point at given index
+ */
+ public double getY(final int i) {
+ return points[normalize(i) * 2 + 1];
+ }
+
+ /**
+ * Returns modified point representing the wrapped point at the given index.
+ *
+ * @param i index of desired point
+ * @param dst point to be modified
+ * @return dst
+ */
+ public Point2D getPoint(final int i, final Point2D dst) {
+ final int pointIndex = normalize(i);
+ dst.setLocation(points[pointIndex * 2], points[pointIndex * 2 + 1]);
+ return dst;
+ }
+
+ /**
+ * Sets the x component of the point at the given index.
+ *
+ * @param i index of point to modify
+ * @param x new x component
+ */
+ public void setX(final int i, final double x) {
+ points[normalize(i) * 2] = x;
+ }
+
+ /**
+ * Sets the y component of the point at the given index.
+ *
+ * @param i index of point to modify
+ * @param y new y component
+ */
+ public void setY(final int i, final double y) {
+ points[normalize(i) * 2 + 1] = y;
+ }
+
+ /**
+ * Sets the coordinates of the point at the given index.
+ *
+ * @param i index of point to modify
+ * @param x new x component
+ * @param y new y component
+ */
+ public void setPoint(final int i, final double x, final double y) {
+ final int pointIndex = normalize(i);
+ points[pointIndex * 2] = x;
+ points[pointIndex * 2 + 1] = y;
+ }
+
+ /**
+ * Sets the coordinates of the point at the given index.
+ *
+ * @param i index of point to modify
+ * @param pt point from which coordinate is to be extracted
+ */
+ public void setPoint(final int i, final Point2D pt) {
+ setPoint(i, pt.getX(), pt.getY());
+ }
+
+ /**
+ * Applies the given transform to all points represented by this XYArray.
+ *
+ * @param t transform to apply
+ */
+ public void transformPoints(final AffineTransform t) {
+ t.transform(points, 0, points, 0, numPoints);
+ }
+
+ /**
+ * Modifies dst to be the bounding box of the points represented by this
+ * XYArray.
+ *
+ * @param dst rectangle to be modified
+ * @return the bounding rectangle
+ */
+ public Rectangle2D getBounds(final Rectangle2D dst) {
+ int i = 0;
+ if (dst.isEmpty() && getPointCount() > 0) {
+ dst.setRect(getX(i), getY(i), 1.0d, 1.0d);
+ i++;
+ }
+ while (i < getPointCount()) {
+ dst.add(getX(i), getY(i));
+ i++;
+ }
+ return dst;
+ }
+
+ /**
+ * Constructs an array of point coordinates for n points and copies the old
+ * values if provided.
+ *
+ * @param points array to populate with point values, or null to generate a
+ * new array
+ * @param n number of points
+ * @param old old values to repopulate the array with, or null if not
+ * desired
+ * @return initialized points
+ */
+ public static double[] initPoints(final double[] points, final int n, final double[] old) {
+ final double[] result;
+ if (points == null || n * 2 > points.length) {
+ result = new double[n * 2];
+ }
+ else {
+ result = points;
+ }
+ if (old != null && result != old) {
+ System.arraycopy(old, 0, result, 0, Math.min(old.length, n * 2));
+ }
+
+ return result;
+ }
+
+ /**
+ * Constructs an array of point coordinates for n points.
+ *
+ * @param srcPoints array to populate with point values, or null to generate
+ * a new array
+ * @param n number of points
+ */
+ private void initPoints(final double[] srcPoints, final int n) {
+ this.points = initPoints(srcPoints, n, this.points);
+ if (srcPoints == null) {
+ numPoints = 0;
+ }
+ else {
+ numPoints = srcPoints.length / 2;
+ }
+ }
+
+ /**
+ * Adds a subsequence of the points provided at the given position.
+ *
+ * @param index position at which the points should be inserted
+ * @param newPoints points from which to extract the subsequence of points
+ * @param start the start index within newPoints to start extracting points
+ * @param end the end index within newPoints to finish extracting points
+ */
+ public void addPoints(final int index, final Points newPoints, final int start, final int end) {
+ final int sanitizedEnd;
+ if (end < 0) {
+ sanitizedEnd = newPoints.getPointCount() + end + 1;
+ }
+ else {
+ sanitizedEnd = end;
+ }
+ final int n = numPoints + sanitizedEnd - start;
+ points = initPoints(points, n, points);
+ final int pos1 = index * 2;
+ final int pos2 = (index + sanitizedEnd - start) * 2;
+ final int len = (numPoints - index) * 2;
+
+ System.arraycopy(points, pos1, points, pos2, len);
+
+ numPoints = n;
+ if (newPoints != null) {
+ for (int count = 0, currentPos = start; currentPos < sanitizedEnd; count++, currentPos++) {
+ setPoint(index + count, newPoints.getX(currentPos), newPoints.getY(currentPos));
+ }
+ }
+ }
+
+ /**
+ * Inserts all the provided points at the given position.
+ *
+ * @param pos index at which to insert the points
+ * @param pts points to be inserted
+ */
+ public void addPoints(final int pos, final Points pts) {
+ addPoints(pos, pts, 0, pts.getPointCount());
+ }
+
+ /**
+ * Adds the provided points to the end of the points.
+ *
+ * @param pts points to be added
+ */
+ public void appendPoints(final Points pts) {
+ addPoints(numPoints, pts);
+ }
+
+ /**
+ * Creates an XYArray representing the given points.
+ *
+ * @param pts points to copy
+ * @return XYArray representing the points provided
+ */
+ public static XYArray copyPoints(final Points pts) {
+ final XYArray newList = new XYArray(pts.getPointCount());
+ newList.appendPoints(pts);
+ return newList;
+ }
+
+ /**
+ * Adds a point to the index provided.
+ *
+ * @param pos index at which to add the point
+ * @param x x coordinate of new point
+ * @param y y coordinate of new point
+ */
+ public void addPoint(final int pos, final double x, final double y) {
+ addPoints(pos, null, 0, 1);
+ setPoint(pos, x, y);
+ }
+
+ /**
+ * Inserts the given point at the given index.
+ *
+ * @param pos index at which to add the point
+ * @param pt point to be inserted *
+ */
+ public void addPoint(final int pos, final Point2D pt) {
+ addPoint(pos, pt.getX(), pt.getY());
+ }
+
+ /**
+ * Remove a subsequence of points from this XYArray starting as pos.
+ *
+ * @param pos the position to start removing points
+ * @param num the number of points to remove
+ */
+ public void removePoints(final int pos, final int num) {
+ int sanitizedNum = Math.min(num, numPoints - pos);
+ if (sanitizedNum > 0) {
+ System.arraycopy(points, (pos + sanitizedNum) * 2, points, pos * 2, (numPoints - (pos + sanitizedNum)) * 2);
+ numPoints -= sanitizedNum;
+ }
+ }
+
+ /**
+ * Remove all points from this XYArray.
+ */
+ public void removeAllPoints() {
+ removePoints(0, numPoints);
+ }
+
+ /**
+ * Returns a clone of this XYArray ensuring a deep copy of coordinates is
+ * made.
+ *
+ * @return cloned XYArray
+ */
+ public Object clone() {
+ XYArray ps = null;
+
+ try {
+ ps = (XYArray) super.clone();
+ ps.points = initPoints(ps.points, numPoints, points);
+ ps.numPoints = numPoints;
+ }
+ catch (final CloneNotSupportedException e) {
+ // wow, this is terrible.
+ }
+
+ return ps;
+ }
+}
diff --git a/src/main/java/edu/umd/cs/piccolox/util/package.html b/src/main/java/edu/umd/cs/piccolox/util/package.html
new file mode 100644
index 0000000..6cd1d17
--- /dev/null
+++ b/src/main/java/edu/umd/cs/piccolox/util/package.html
@@ -0,0 +1,34 @@
+
+
+
This package defines additional utility classes that are likely to be useful for Piccolo applications.
+ + diff --git a/src/main/java/jp/.DS_Store b/src/main/java/jp/.DS_Store new file mode 100644 index 0000000..7688302 --- /dev/null +++ b/src/main/java/jp/.DS_Store Binary files differ diff --git a/src/main/java/jp/ac/.DS_Store b/src/main/java/jp/ac/.DS_Store new file mode 100644 index 0000000..e1288fa --- /dev/null +++ b/src/main/java/jp/ac/.DS_Store Binary files differ diff --git a/src/main/java/jp/ac/kyutech/.DS_Store b/src/main/java/jp/ac/kyutech/.DS_Store new file mode 100644 index 0000000..0c0ef52 --- /dev/null +++ b/src/main/java/jp/ac/kyutech/.DS_Store Binary files differ diff --git a/src/main/java/jp/ac/kyutech/mns/.DS_Store b/src/main/java/jp/ac/kyutech/mns/.DS_Store new file mode 100644 index 0000000..ed07d6a --- /dev/null +++ b/src/main/java/jp/ac/kyutech/mns/.DS_Store Binary files differ diff --git a/src/main/java/jp/ac/kyutech/mns/ist/ATN_Mysql_DB.java b/src/main/java/jp/ac/kyutech/mns/ist/ATN_Mysql_DB.java new file mode 100644 index 0000000..f1e5dd6 --- /dev/null +++ b/src/main/java/jp/ac/kyutech/mns/ist/ATN_Mysql_DB.java @@ -0,0 +1,444 @@ +package jp.ac.kyutech.mns.ist; + +import java.awt.Color; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.TimeZone; + +import javax.swing.JFrame; +import javax.swing.JOptionPane; + +import jp.ac.kyutech.mns.ist.util.SingleSelector; + +import com.mysql.jdbc.exceptions.jdbc4.CommunicationsException; + +/** + * MySQL接続クラス + * @author miuramo + * + */ +public class ATN_Mysql_DB { + String user; + String pass; + String host = "localhost"; + String db; + + Connection con; + Statement stmt; + + SimpleATN satn; + // static String nullorzero_with_is = "is NULL"; + // static String nullorzero = "NULL"; + static String nullorzero_with_is = "= 0"; + static String nullorzero = "0"; + + public ATN_Mysql_DB(SimpleATN _satn, String _user, String _pass, String _db){ + satn = _satn; + user = _user; + pass = _pass; + db = _db; + + reconnect(); + } + public void reconnect(){ + try { + Class.forName("com.mysql.jdbc.Driver"); // MySQLの場合 + String url = "jdbc:mysql://" + host + "/" + + db + "?user=" + + user + "&password=" + + pass + + "&useUnicode=true&characterEncoding=SJIS"; + con = (Connection) DriverManager.getConnection(url); + + // ステートメントオブジェクトを生成 + stmt = (Statement) con.createStatement(); + } catch (CommunicationsException ex){ + satn.menutoolbar.disableDBMenus(); + satn.loadFromFile(null,2); + return; + } catch (Exception e) { + satn.findDBName(); + e.printStackTrace(); + } + } + public void import_file(JFrame parent){ + String dbname = JOptionPane.showInputDialog(parent, "新規データベース名を入力してください"); + if (dbname == null) { + JOptionPane.showMessageDialog(parent, "インポートをキャンセルしました"); + return; + } + //同一名のDBがないかチェック + ArrayList