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);
- }
- }
-
- /**
- * 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;
- }
-
- /**
- * 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();
- }
+ /** 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 Listnull 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);
+ }
+ }
+
+ /**
+ * Return a reference to the list of layers viewed by this camera.
+ *
+ * @return the list of layers viewed by this camera
+ */
+ public Listindex < 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 = 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+ * 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;
+ }
+
+ /**
+ * 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
- * 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(); - } - - // **************************************************************** - // 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 an array of all the property change listeners added to this node. - *
- * If some listeners have been added with a named property, then
- * the returned array will be a mixture of PropertyChangeListeners
- * and PropertyChangeListenerProxys. If the calling
- * method is interested in distinguishing the listeners then it must
- * test each element to see if it is a PropertyChangeListenerProxy,
- * perform the cast, and examine the parameter.
- *
- *
- * PropertyChangeListener[] listeners = bean.getPropertyChangeListeners();
- * for (int i = 0; i < listeners.length; i++) {
- * if (listeners[i] instanceof PropertyChangeListenerProxy) {
- * PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) listeners[i];
- * if (proxy.getPropertyName().equals("foo")) {
- * // proxy is a PropertyChangeListener which was associated
- * // with the property named "foo"
- * }
- * }
- * }
- *
- *
- * @since 3.0.1
- * @return all of the PropertyChangeListeners added or an
- * empty array if no listeners have been added
- */
- public PropertyChangeListener[] getPropertyChangeListeners() {
- if (changeSupport == null) {
- return new PropertyChangeListener[0];
- }
- return changeSupport.getPropertyChangeListeners();
- }
-
- /**
- * Return an array of all the property change listeners which have been
- * associated with the named property.
- *
- * @since 3.0.1
- * @param propertyName the name of the property being listened to
- * @return all of the PropertyChangeListeners associated with
- * the named property. If no such listeners have been added,
- * or if propertyName is null, an empty array is
- * returned.
- */
- public PropertyChangeListener[] getPropertyChangeListeners(final String propertyName) {
- if (changeSupport == null) {
- return new PropertyChangeListener[0];
- }
- return changeSupport.getPropertyChangeListeners(propertyName);
- }
-
- /**
- * 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++) {
- 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++) {
- 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); - } - } - - /** - * 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.
- * @throws PrinterException if print fails
- */
- public void print() throws PrinterException {
- 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()) {
- printJob.print();
- }
- }
-
- /**
- * 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;
- }
-
- /**
- * Raise this node within the Z-order of its parent.
- *
- * @since 3.0
- */
- public void raise() {
- final PNode p = parent;
- if (p != null) {
- final int index = parent.indexOfChild(this);
- final int siblingIndex = Math.min(parent.getChildrenCount() - 1, index + 1);
- if (siblingIndex != index) {
- raiseAbove(parent.getChild(siblingIndex));
- }
- }
- }
-
- /**
- * Lower this node within the Z-order of its parent.
- *
- * @since 3.0
- */
- public void lower() {
- final PNode p = parent;
- if (p != null) {
- final int index = parent.indexOfChild(this);
- final int siblingIndex = Math.max(0, index - 1);
- if (siblingIndex != index) {
- lowerBelow(parent.getChild(siblingIndex));
- }
- }
- }
-
- /**
- * Raise this node within the Z-order of its parent to the top.
- *
- * @since 3.0
- */
- public void raiseToTop() {
- final PNode p = parent;
- if (p != null) {
- p.removeChild(this);
- p.addChild(this);
- }
- }
-
- /**
- * Lower this node within the Z-order of its parent to the bottom.
- *
- * @since 3.0
- */
- public void lowerToBottom() {
- final PNode p = parent;
- if (p != null) {
- p.removeChild(this);
- p.addChild(0, this);
- }
- }
-
- /**
- * Raise this node within the Z-order of its parent above the specified sibling node.
- *
- * @since 3.0
- * @param sibling sibling node to raise this node above
- */
- public void raiseAbove(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);
- }
- }
-
- /**
- * Lower this node within the Z-order of its parent below the specified sibling node.
- *
- * @since 3.0
- * @param sibling sibling node to lower this node below
- */
- public void lowerBelow(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);
- }
- }
-
- /**
- * Raise the specified child node within the Z-order of this.
- *
- * @since 3.0
- * @param child child node to raise
- */
- public void raise(final PNode child) {
- if (children != null && children.contains(child) && this.equals(child.getParent())) {
- child.raise();
- }
- }
-
- /**
- * Lower the specified child node within the Z-order of this.
- *
- * @since 3.0
- * @param child child node to lower
- */
- public void lower(final PNode child) {
- if (children != null && children.contains(child) && this.equals(child.getParent())) {
- child.lower();
- }
- }
-
- /**
- * Raise the specified child node within the Z-order of this to the top.
- *
- * @since 3.0
- * @param child child node to raise to the top
- */
- public void raiseToTop(final PNode child) {
- if (children != null && children.contains(child) && this.equals(child.getParent())) {
- child.raiseToTop();
- }
- }
-
- /**
- * Lower the specified child node within the Z-order of this to the bottom.
- *
- * @since 3.0
- * @param child child node to lower to the bottom
- */
- public void lowerToBottom(final PNode child) {
- if (children != null && children.contains(child) && this.equals(child.getParent())) {
- child.lowerToBottom();
- }
- }
-
- /**
- * 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 node, 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();
- }
-
- /**
- * 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;
- }
-
- /**
- * 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);
- }
+ /**
+ * 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 properties
+ * (see {@link #getClientProperties getClientProperties}). 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 properties
+ * (see {@link #getClientProperties getClientProperties}). 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
+ * 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(); + } + + // **************************************************************** + // 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 an array of all the property change listeners added to this node. + *
+ * If some listeners have been added with a named property, then the returned
+ * array will be a mixture of PropertyChangeListeners and
+ * PropertyChangeListenerProxys. If the calling method is
+ * interested in distinguishing the listeners then it must test each element to
+ * see if it is a PropertyChangeListenerProxy, perform the cast,
+ * and examine the parameter.
+ *
+ *
+ * PropertyChangeListener[] listeners = bean.getPropertyChangeListeners();
+ * for (int i = 0; i < listeners.length; i++) {
+ * if (listeners[i] instanceof PropertyChangeListenerProxy) {
+ * PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) listeners[i];
+ * if (proxy.getPropertyName().equals("foo")) {
+ * // proxy is a PropertyChangeListener which was associated
+ * // with the property named "foo"
+ * }
+ * }
+ * }
+ *
+ *
+ * @since 3.0.1
+ * @return all of the PropertyChangeListeners added or an empty
+ * array if no listeners have been added
+ */
+ public PropertyChangeListener[] getPropertyChangeListeners() {
+ if (changeSupport == null) {
+ return new PropertyChangeListener[0];
+ }
+ return changeSupport.getPropertyChangeListeners();
+ }
+
+ /**
+ * Return an array of all the property change listeners which have been
+ * associated with the named property.
+ *
+ * @since 3.0.1
+ * @param propertyName the name of the property being listened to
+ * @return all of the PropertyChangeListeners associated with the
+ * named property. If no such listeners have been added, or if
+ * propertyName is null, an empty array is returned.
+ */
+ public PropertyChangeListener[] getPropertyChangeListeners(final String propertyName) {
+ if (changeSupport == null) {
+ return new PropertyChangeListener[0];
+ }
+ return changeSupport.getPropertyChangeListeners(propertyName);
+ }
+
+ /**
+ * 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++) {
+ 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++) {
+ 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); + } + } + + /** + * 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, Float.valueOf(oldTransparency), + Float.valueOf(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.
+ *
+ * @throws PrinterException if print fails
+ */
+ public void print() throws PrinterException {
+ 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()) {
+ printJob.print();
+ }
+ }
+
+ /**
+ * 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 ArrayListPPaintContext.HIGH_QUALITY_RENDERING. */
- static final int DEFAULT_RENDER_QUALITY = PPaintContext.HIGH_QUALITY_RENDERING;
-
- /** 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;
-
- /** True if this offscreen canvas is opaque. */
- private boolean opaque;
-
- /** Background color for this offscreen canvas. */
- private Color backgroundColor;
-
-
- /**
- * 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());
-
- opaque = false;
- backgroundColor = null;
- }
-
-
- /**
- * 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");
- }
-
- if (opaque && backgroundColor != null) {
- graphics.setBackground(backgroundColor);
- graphics.clearRect(0, 0, (int) bounds.getWidth(), (int) bounds.getHeight());
- }
-
- 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
- }
-
- /**
- * Return the root node of the scene graph for this offscreen canvas. The
- * root node will be null if the camera for this offscreen canvas is null.
- *
- * @return the root node of the scene graph for this offscreen canvas
- */
- public PRoot getRoot() {
- return camera == null ? null : camera.getRoot();
- }
-
- /**
- * Return true if this offscreen canvas is opaque. Defaults to false.
- *
- * @return true if this offscreen canvas is opaque
- */
- public boolean isOpaque() {
- return opaque;
- }
-
- /**
- * Set to true if this offscreen canvas is opaque.
- *
- * @param opaque true if this offscreen canvas is opaque
- */
- public void setOpaque(final boolean opaque) {
- this.opaque = opaque;
- }
-
- /**
- * Return the background color for this offscreen canvas. If this
- * offscreen canvas is opaque, the background color will be painted
- * before the contents of the scene are rendered.
- *
- * @see #isOpaque
- * @return the background color for this offscreen canvas
- */
- public Color getBackground() {
- return backgroundColor;
- }
-
- /**
- * Set the background color for this offscreen canvas to backgroundColor.
- * If this offscreen canvas is opaque, the background color will be painted
- * before the contents of the scene are rendered.
- *
- * @see #isOpaque
- * @param backgroundColor background color for this offscreen canvas
- */
- public void setBackground(final Color backgroundColor) {
- this.backgroundColor = backgroundColor;
- }
+ /**
+ * Default render quality, PPaintContext.HIGH_QUALITY_RENDERING.
+ */
+ static final int DEFAULT_RENDER_QUALITY = PPaintContext.HIGH_QUALITY_RENDERING;
+
+ /** 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;
+
+ /** True if this offscreen canvas is opaque. */
+ private boolean opaque;
+
+ /** Background color for this offscreen canvas. */
+ private Color backgroundColor;
+
+ /**
+ * 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());
+
+ opaque = false;
+ backgroundColor = null;
+ }
+
+ /**
+ * 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");
+ }
+
+ if (opaque && backgroundColor != null) {
+ graphics.setBackground(backgroundColor);
+ graphics.clearRect(0, 0, (int) bounds.getWidth(), (int) bounds.getHeight());
+ }
+
+ 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
+ }
+
+ /**
+ * Return the root node of the scene graph for this offscreen canvas. The root
+ * node will be null if the camera for this offscreen canvas is null.
+ *
+ * @return the root node of the scene graph for this offscreen canvas
+ */
+ public PRoot getRoot() {
+ return camera == null ? null : camera.getRoot();
+ }
+
+ /**
+ * Return true if this offscreen canvas is opaque. Defaults to
+ * false.
+ *
+ * @return true if this offscreen canvas is opaque
+ */
+ public boolean isOpaque() {
+ return opaque;
+ }
+
+ /**
+ * Set to true if this offscreen canvas is opaque.
+ *
+ * @param opaque true if this offscreen canvas is opaque
+ */
+ public void setOpaque(final boolean opaque) {
+ this.opaque = opaque;
+ }
+
+ /**
+ * Return the background color for this offscreen canvas. If this offscreen
+ * canvas is opaque, the background color will be painted before the contents of
+ * the scene are rendered.
+ *
+ * @see #isOpaque
+ * @return the background color for this offscreen canvas
+ */
+ public Color getBackground() {
+ return backgroundColor;
+ }
+
+ /**
+ * Set the background color for this offscreen canvas to
+ * backgroundColor. If this offscreen canvas is opaque, the
+ * background color will be painted before the contents of the scene are
+ * rendered.
+ *
+ * @see #isOpaque
+ * @param backgroundColor background color for this offscreen canvas
+ */
+ public void setBackground(final Color backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ }
}
diff --git a/core/src/main/java/org/piccolo2d/PRoot.java b/core/src/main/java/org/piccolo2d/PRoot.java
index e16d3d75..2bd5a92c 100644
--- a/core/src/main/java/org/piccolo2d/PRoot.java
+++ b/core/src/main/java/org/piccolo2d/PRoot.java
@@ -41,7 +41,6 @@
import org.piccolo2d.util.PDebug;
import org.piccolo2d.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
@@ -53,366 +52,362 @@
*/
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.
- *
- * This has similar functionality to the setInteracting method on Canvas,
- * but this is the appropriate place to mark interactions that may occur in
- * multiple canvases if this Root is shared.
- *
- * @since 1.3
- * @param isInteracting True if this root has user interaction taking place
- * @see PCanvas#setInteracting(boolean)
- */
- public void setInteracting(final boolean isInteracting) {
- final boolean wasInteracting = getInteracting();
-
- if (isInteracting) {
- interacting++;
- }
- else {
- interacting--;
- }
-
- if (!isInteracting && !getInteracting()) {
- // force all the child cameras to repaint
- for (int i = 0; i < getChildrenCount(); i++) {
- final PNode child = getChild(i);
- if (child instanceof PCamera) {
- child.repaint();
- }
- }
-
- }
- if (wasInteracting != isInteracting) {
- firePropertyChange(PROPERTY_CODE_INTERACTING_CHANGED, PROPERTY_INTERACTING_CHANGED, Boolean
- .valueOf(wasInteracting), Boolean.valueOf(isInteracting));
- }
- }
-
- /**
- * Advanced. If you want to add additional input sources to the roots UI
- * process you can do that here. You will seldom do this unless you are
- * making additions to the Piccolo2D framework.
- *
- * @param inputSource An input source that should be added
- */
- public void addInputSource(final InputSource inputSource) {
- inputSources.add(inputSource);
- firePropertyChange(PROPERTY_CODE_INPUT_SOURCES, PROPERTY_INPUT_SOURCES, null, inputSources);
- }
-
- /**
- * Advanced. If you want to remove the default input source from the roots
- * UI process you can do that here. You will seldom do this unless you are
- * making additions to the Piccolo2D framework.
- *
- * @param inputSource input source that should no longer be asked about
- * input events
- */
- public void removeInputSource(final InputSource inputSource) {
- if (inputSources.remove(inputSource)) {
- firePropertyChange(PROPERTY_CODE_INPUT_SOURCES, PROPERTY_INPUT_SOURCES, null, inputSources);
- }
- }
-
- /**
- * Returns a new timer. This method allows subclasses, such as PSWTRoot to
- * create custom timers that will be used transparently by the Piccolo2D
- * framework.
- *
- * @param delay # of milliseconds before action listener is invoked
- * @param listener listener to be invoked after delay
- *
- * @return A new Timer
- */
- public Timer createTimer(final int delay, final ActionListener listener) {
- return new Timer(delay, listener);
- }
-
- // ****************************************************************
- // UI Loop - Methods for running the main UI loop of Piccolo2D.
- // ****************************************************************
-
- /**
- * Get the global Piccolo2D time. This is set to System.currentTimeMillis()
- * at the beginning of the roots
+ * This has similar functionality to the setInteracting method on Canvas, but
+ * this is the appropriate place to mark interactions that may occur in multiple
+ * canvases if this Root is shared.
+ *
+ * @since 1.3
+ * @param isInteracting True if this root has user interaction taking place
+ * @see PCanvas#setInteracting(boolean)
+ */
+ public void setInteracting(final boolean isInteracting) {
+ final boolean wasInteracting = getInteracting();
+
+ if (isInteracting) {
+ interacting++;
+ } else {
+ interacting--;
+ }
+
+ if (!isInteracting && !getInteracting()) {
+ // force all the child cameras to repaint
+ for (int i = 0; i < getChildrenCount(); i++) {
+ final PNode child = getChild(i);
+ if (child instanceof PCamera) {
+ child.repaint();
+ }
+ }
+
+ }
+ if (wasInteracting != isInteracting) {
+ firePropertyChange(PROPERTY_CODE_INTERACTING_CHANGED, PROPERTY_INTERACTING_CHANGED,
+ Boolean.valueOf(wasInteracting), Boolean.valueOf(isInteracting));
+ }
+ }
+
+ /**
+ * Advanced. If you want to add additional input sources to the roots UI process
+ * you can do that here. You will seldom do this unless you are making additions
+ * to the Piccolo2D framework.
+ *
+ * @param inputSource An input source that should be added
+ */
+ public void addInputSource(final InputSource inputSource) {
+ inputSources.add(inputSource);
+ firePropertyChange(PROPERTY_CODE_INPUT_SOURCES, PROPERTY_INPUT_SOURCES, null, inputSources);
+ }
+
+ /**
+ * Advanced. If you want to remove the default input source from the roots UI
+ * process you can do that here. You will seldom do this unless you are making
+ * additions to the Piccolo2D framework.
+ *
+ * @param inputSource input source that should no longer be asked about input
+ * events
+ */
+ public void removeInputSource(final InputSource inputSource) {
+ if (inputSources.remove(inputSource)) {
+ firePropertyChange(PROPERTY_CODE_INPUT_SOURCES, PROPERTY_INPUT_SOURCES, null, inputSources);
+ }
+ }
+
+ /**
+ * Returns a new timer. This method allows subclasses, such as PSWTRoot to
+ * create custom timers that will be used transparently by the Piccolo2D
+ * framework.
+ *
+ * @param delay # of milliseconds before action listener is invoked
+ * @param listener listener to be invoked after delay
+ *
+ * @return A new Timer
+ */
+ public Timer createTimer(final int delay, final ActionListener listener) {
+ return new Timer(delay, listener);
+ }
+
+ // ****************************************************************
+ // UI Loop - Methods for running the main UI loop of Piccolo2D.
+ // ****************************************************************
+
+ /**
+ * Get the global Piccolo2D time. This is set to System.currentTimeMillis() at
+ * the beginning of the roots 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();
- }
+ /**
+ * 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 ListprocessInputs 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 IteratorprocessInputs 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