/*
* Place2.java
* Transform
*
* Copyright (c) 2001-2010 Flagstone Software Ltd. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Flagstone Software Ltd. nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.flagstone.transform;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.flagstone.transform.coder.Coder;
import com.flagstone.transform.coder.Context;
import com.flagstone.transform.coder.SWFDecoder;
import com.flagstone.transform.coder.SWFEncoder;
import com.flagstone.transform.datatype.ColorTransform;
import com.flagstone.transform.datatype.CoordTransform;
import com.flagstone.transform.exception.IllegalArgumentRangeException;
/**
* Place2 is used to add and manipulate objects (shape, button, etc.) on
* the Flash Player's display list.
*
* <p>
* Place2 supersedes the Place class providing more functionality
* and easier manipulation of objects in the display list through the following
* operations:
* </p>
*
* <ul>
* <li>Place a new shape on the display list.</li>
* <li>Change an existing shape by moving it to new location or changing its
* appearance.</li>
* <li>Replace an existing shape with a another.</li>
* <li>
* Define clipping layers to mask objects displayed in front of a shape.</li>
* <li>Control the morphing process that changes one shape into another.</li>
* <li>Assign names to objects rather than using their identifiers.</li>
* <li>Define the sequence of actions that are executed when an event occurs in
* movie clip.</li>
* </ul>
*
* <p>
* <b>Clipping Depth</b><br/>
* With the introduction of Flash 3 the display list supported a clipping layer.
* This allowed the outline of an object to define a clipping path that is used
* to mask other objects placed in front of it. The clipping depth can be set to
* mask objects between the layer containing the clipping path and a specified
* layer.
* </p>
*
* <p>
* <b>Shape Morphing</b><br/>
* Shapes that will be morphed are defined using the DefineMorphShape class
* which defines a start and end shape. The Flash Player performs the
* interpolation that transforms one shape into another. The progress of the
* morphing process is controlled by a ratio which ranges from 0.0 to 1.0, where
* 0 generates a shape identical to the starting shape in the DefineMorphShape
* object and 1.0 generates the shape at the end of the morphing process.
* </p>
*
* <p>
* <b>Event Handlers</b><br/>
* With the introduction of Flash 5, movie clips (defined using the
* DefineMovieClip class) could specify sequences of actions that would be
* performed in response to mouse or keyboard events. The actions are specified
* using EventHandler objects and the Place2 class is used to register the
* actions in response to a particular event with the Flash player. Multiple
* events can be handled by defining an EventHandler for each type of event. For
* more information see the EventHandler and Event which defines the set of
* events that a movie clip responds to.
* </p>
*
* <p>
* Since only one object can be placed on a given layer an existing object on
* the display list can be identified by the layer it is displayed on rather
* than its identifier. Therefore Layer is the only required attribute. The
* remaining attributes are optional according to the different operation being
* performed:
* </p>
*
* <ul>
* <li>If an existing object on the display list is being modified then only the
* layer number is required. Previously in the Place class both the
* identifier and the layer number were required.</li>
* <li>If no coordinate transform is applied to the shape (the default is a
* unity transform that does not change the shape) then it is not encoded.</li>
* <li>Similarly if no colour transform is applied to the shape (the default is
* a unity transform that does not change the shape's colour) then it is not
* encoded.</li>
* <li>If a shape is not being morphed then the ratio attribute may be left at
* its default value (-1.0).</li>
* <li>If a shape is not used to define a clipping area then the depth attribute
* may be left at its default value (0).</li>
* <li>If a name is net assigned to an object the name attribute may be left its
* default value (an empty string).</li>
* <li>If no events are being defined for a movie clip then the list of
* ClipEvent object may be left empty.</li>
* </ul>
*
* <p>
* The Layer class provides a simple API for manipulating objects on the display
* list. While it is relatively simple to create instances of Place2
* object that perform the same steps the API provided by Player is easier to
* use and much more readable.
* </p>
*
* @see com.flagstone.transform.util.movie.Layer
*/
@SuppressWarnings({"PMD.TooManyMethods", "PMD.CyclomaticComplexity" })
public final class Place2 implements MovieTag {
/**
* Place a new object on the display list.
* @param identifier the unique identifier for the object.
* @param layer the layer where it will be displayed.
* @param xCoord the x-coordinate where the object's origin will be.
* @param yCoord the y-coordinate where the object's origin will be.
* @return the Place2 object to update the display list.
*/
public static Place2 show(final int identifier, final int layer,
final int xCoord, final int yCoord) {
final Place2 object = new Place2();
object.setType(PlaceType.NEW);
object.setLayer(layer);
object.setIdentifier(identifier);
object.setTransform(CoordTransform.translate(xCoord, yCoord));
return object;
}
/**
* Change the position of a displayed object.
*
* @param layer the display list layer where the object is displayed.
* @param xCoord the x-coordinate where the object's origin will be moved.
* @param yCoord the y-coordinate where the object's origin will be moved.
* @return the Place3 object to change the position of the object.
*/
public static Place2 move(final int layer, final int xCoord,
final int yCoord) {
final Place2 object = new Place2();
object.setType(PlaceType.MODIFY);
object.setLayer(layer);
object.setTransform(CoordTransform.translate(xCoord, yCoord));
return object;
}
/**
* Replace an existing object with another.
*
* @param identifier the unique identifier of the new object.
* @param layer the display list layer of the existing object.
* @return the Place3 object to update the display list.
*/
public static Place2 replace(final int identifier, final int layer) {
final Place2 object = new Place2();
object.setType(PlaceType.REPLACE);
object.setLayer(layer);
object.setIdentifier(identifier);
return object;
}
/**
* Replace an existing object with another.
*
* @param identifier the unique identifier of the new object.
* @param layer the display list layer of the existing object.
* @param xCoord the x-coordinate where the new object's origin will be.
* @param yCoord the y-coordinate where the new object's origin will be.
* @return the Place3 object to update the display list.
*/
public static Place2 replace(final int identifier, final int layer,
final int xCoord, final int yCoord) {
final Place2 object = new Place2();
object.setType(PlaceType.REPLACE);
object.setLayer(layer);
object.setIdentifier(identifier);
object.setTransform(CoordTransform.translate(xCoord, yCoord));
return object;
}
/** The version of Flash where standard (16-bit) event codes were used. */
private static final int STANDARD_EVENTS = 5;
/** Format string used in toString() method. */
private static final String FORMAT = "Place2: { type=%s; layer=%d;"
+ " identifier=%d; transform=%s; colorTransform=%s; ratio=%d;"
+ " clippingDepth=%d; name=%s; clipEvents=%s}";
/** How the display list will be updated. */
private PlaceType type;
/** The display list layer number. */
private int layer;
/** The unique identifier of the object that will be displayed. */
private int identifier;
/** The coordinate transform applied to the displayed object. */
private CoordTransform transform;
/** The color transform applied to the displayed object. */
private ColorTransform colorTransform;
/** The progression of the morphing process. */
private Integer ratio;
/** The number of layers to clip. */
private Integer depth;
/** The name assigned to an object. */
private String name;
/** The set of event handlers for movie clips. */
private List<EventHandler> events;
/** The length of the object, minus the header, when it is encoded. */
private transient int length;
/**
* Creates and initialises a Place2 object using values encoded
* in the Flash binary format.
*
* @param coder
* an SWFDecoder object that contains the encoded Flash data.
*
* @param context
* a Context object used to manage the decoders for different
* type of object and to pass information on how objects are
* decoded.
*
* @throws IOException
* if an error occurs while decoding the data.
*/
@SuppressWarnings("PMD.AssignmentInOperand")
public Place2(final SWFDecoder coder, final Context context)
throws IOException {
context.put(Context.TRANSPARENT, 1);
length = coder.readUnsignedShort() & Coder.LENGTH_FIELD;
if (length == Coder.IS_EXTENDED) {
length = coder.readInt();
}
coder.mark();
final int bits = coder.readByte();
final boolean hasEvents = (bits & Coder.BIT7) != 0;
final boolean hasDepth = (bits & Coder.BIT6) != 0;
final boolean hasName = (bits & Coder.BIT5) != 0;
final boolean hasRatio = (bits & Coder.BIT4) != 0;
final boolean hasColorTransform = (bits & Coder.BIT3) != 0;
final boolean hasTransform = (bits & Coder.BIT2) != 0;
switch (bits & Coder.PAIR0) {
case 0:
type = PlaceType.MODIFY;
break;
case 1:
type = PlaceType.MODIFY;
break;
case 2:
type = PlaceType.NEW;
break;
default:
type = PlaceType.REPLACE;
break;
}
layer = coder.readUnsignedShort();
events = new ArrayList<EventHandler>();
if ((type == PlaceType.NEW) || (type == PlaceType.REPLACE)) {
identifier = coder.readUnsignedShort();
}
if (hasTransform) {
transform = new CoordTransform(coder);
}
if (hasColorTransform) {
colorTransform = new ColorTransform(coder, context);
}
if (hasRatio) {
ratio = coder.readUnsignedShort();
}
if (hasName) {
name = coder.readString();
}
if (hasDepth) {
depth = coder.readUnsignedShort();
}
if (hasEvents) {
int event;
coder.readUnsignedShort();
if (context.get(Context.VERSION) > STANDARD_EVENTS) {
coder.readInt();
while ((event = coder.readInt()) != 0) {
events.add(new EventHandler(event,
coder, context));
}
} else {
coder.readUnsignedShort();
while ((event = coder.readUnsignedShort()) != 0) {
events.add(new EventHandler(event,
coder, context));
}
}
}
context.remove(Context.TRANSPARENT);
coder.check(length);
coder.unmark();
}
/**
* Creates an uninitialised Place2 object.
*/
public Place2() {
events = new ArrayList<EventHandler>();
}
/**
* Creates and initialises a Place2 object using the values copied
* from another Place2 object.
*
* @param object
* a Place2 object from which the values will be
* copied.
*/
public Place2(final Place2 object) {
type = object.type;
layer = object.layer;
identifier = object.identifier;
if (object.transform != null) {
transform = object.transform;
}
if (object.colorTransform != null) {
colorTransform = object.colorTransform;
}
ratio = object.ratio;
depth = object.depth;
name = object.name;
events = new ArrayList<EventHandler>(object.events.size());
for (final EventHandler event : object.events) {
events.add(event.copy());
}
}
/**
* Adds a clip event to the list of clip events.
*
* @param aClipEvent
* a clip event object.
*
* @return this object.
*/
public Place2 add(final EventHandler aClipEvent) {
if (aClipEvent == null) {
throw new IllegalArgumentException();
}
events.add(aClipEvent);
return this;
}
/**
* Get the list of event handlers that define the actions that will
* be executed in response to events that occur in the movie clip being
* placed.
*
* @return the set of event handlers for the movie clip.
*/
public List<EventHandler> getEvents() {
return events;
}
/**
* Set the list of Clip events. Clip Events are only valid for movie clips
* and the argument should be set to null when placing other types of
* object.
*
* If the object already contains a set of encoded clip event objects they
* will be deleted.
*
* @param list
* a list of ClipEvent objects.
*/
public void setEvents(final List<EventHandler> list) {
if (list == null) {
throw new IllegalArgumentException();
}
events = list;
}
/**
* Get the type of place operation being performed, either adding a new
* object, replacing an existing one with another or modifying an existing
* object.
*
* @return the way the object will be placed.
*/
public PlaceType getType() {
return type;
}
/**
* Get the Layer on which the object will be displayed in the display
* list.
*
* @return the layer where the object will be displayed.
*/
public int getLayer() {
return layer;
}
/**
* Get the identifier of the object to be placed. This is only required
* when placing an object for the first time. Subsequent references to the
* object on this layer can simply use the layer number.
*
* @return the unique identifier of the object to be displayed.
*/
public int getIdentifier() {
return identifier;
}
/**
* Get the coordinate transform. May be null if no coordinate transform
* was defined.
*
* @return the coordinate transform that will be applied to the displayed
* object.
*/
public CoordTransform getTransform() {
return transform;
}
/**
* Get the colour transform. May be null if no colour transform was
* defined.
*
* @return the colour transform that will be applied to the displayed
* object.
*/
public ColorTransform getColorTransform() {
return colorTransform;
}
/**
* Get the morph ratio, in the range 0..65535 that defines the progress
* in the morphing process performed by the Flash Player from the defined
* start and end shapes. A value of 0 indicates the start of the process and
* 65535 the end. Returns null if no ratio was specified.
*
* @return the morphing ratio.
*/
public Integer getRatio() {
return ratio;
}
/**
* Get the number of layers that will be clipped by the object placed on
* the layer specified in this object.
*
* @return the number of layers to be clipped.
*/
public Integer getDepth() {
return depth;
}
/**
* Get the name of the object. May be null if a name was not assigned to
* the object.
*
* @return the name of the object.
*/
public String getName() {
return name;
}
/**
* Sets the type of placement.
*
* @param aType
* the type of operation to be performed, either New, Modify or
* Replace.
*
* @return this object.
*/
public Place2 setType(final PlaceType aType) {
type = aType;
return this;
}
/**
* Sets the layer at which the object will be placed.
*
* @param aLayer
* the layer number on which the object is being displayed. Must
* be in the range 1..65535.
*
* @return this object.
*/
public Place2 setLayer(final int aLayer) {
if ((aLayer < 1) || (aLayer > Coder.USHORT_MAX)) {
throw new IllegalArgumentRangeException(
1, Coder.USHORT_MAX, aLayer);
}
layer = aLayer;
return this;
}
/**
* Sets the identifier of the object.
*
* @param uid
* the identifier of a new object to be displayed. Must be in the
* range 1..65535.
*
* @return this object.
*/
public Place2 setIdentifier(final int uid) {
if ((uid < 1) || (uid > Coder.USHORT_MAX)) {
throw new IllegalArgumentRangeException(
1, Coder.USHORT_MAX, uid);
}
identifier = uid;
return this;
}
/**
* Sets the coordinate transform that defines the position where the object
* will be displayed. The argument may be null if the location of the object
* is not being changed.
*
* @param matrix
* an CoordTransform object that will be applied to the object
* displayed.
*
* @return this object.
*/
public Place2 setTransform(final CoordTransform matrix) {
transform = matrix;
return this;
}
/**
* Sets the location where the object will be displayed.
*
* @param xCoord
* the x-coordinate of the object's origin.
* @param yCoord
* the x-coordinate of the object's origin.
* @return this object.
*/
public Place2 setLocation(final int xCoord, final int yCoord) {
transform = CoordTransform.translate(xCoord, yCoord);
return this;
}
/**
* Sets the colour transform that defines the colour effects applied to the
* object. The argument may be null if the color of the object is not being
* changed.
*
* @param cxform
* an ColorTransform object that will be applied to the object
* displayed.
*
* @return this object.
*/
public Place2 setColorTransform(final ColorTransform cxform) {
colorTransform = cxform;
return this;
}
/**
* Sets point of the morphing process for a morph shape in the range
* 0..65535. May be set to null if the shape being placed is not being
* morphed.
*
* @param aNumber
* the progress in the morphing process.
*
* @return this object.
*/
public Place2 setRatio(final Integer aNumber) {
if ((aNumber != null) && ((aNumber < 0)
|| (aNumber > Coder.USHORT_MAX))) {
throw new IllegalArgumentRangeException(
1, Coder.USHORT_MAX, aNumber);
}
ratio = aNumber;
return this;
}
/**
* Sets the number of layers that this object will mask. May be set to zero
* if the shape being placed does not define a clipping area.
*
* @param aNumber
* the number of layers clipped.
*
* @return this object.
*/
public Place2 setDepth(final Integer aNumber) {
if ((aNumber != null) && ((aNumber < 1)
|| (aNumber > Coder.USHORT_MAX))) {
throw new IllegalArgumentRangeException(
1, Coder.USHORT_MAX, aNumber);
}
depth = aNumber;
return this;
}
/**
* Set the name of an object to be displayed. If a shape is not being
* assigned a name then setting the argument to null will omit the attribute
* when the object is encoded.
*
* @param aString
* the name assigned to the object.
*
* @return this object.
*/
public Place2 setName(final String aString) {
name = aString;
return this;
}
/** {@inheritDoc} */
@Override
public Place2 copy() {
return new Place2(this);
}
/** {@inheritDoc} */
@Override
public String toString() {
return String.format(FORMAT, type, layer, identifier, transform,
colorTransform, ratio, depth, name, events);
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("PMD.NPathComplexity")
public int prepareToEncode(final Context context) {
// CHECKSTYLE:OFF
context.put(Context.TRANSPARENT, 1);
length = 3;
length += (type.equals(PlaceType.NEW) || type
.equals(PlaceType.REPLACE)) ? 2 : 0;
if (transform != null) {
length += transform.prepareToEncode(context);
}
if (colorTransform != null) {
length += colorTransform.prepareToEncode(context);
}
length += ratio == null ? 0 : 2;
length += depth == null ? 0 : 2;
length += name == null ? 0 : context.strlen(name);
if (!events.isEmpty()) {
int eventSize;
if (context.get(Context.VERSION) > STANDARD_EVENTS) {
eventSize = 4;
} else {
eventSize = 2;
}
length += 2 + eventSize;
for (final EventHandler handler : events) {
length += handler.prepareToEncode(context);
}
length += eventSize;
}
context.remove(Context.TRANSPARENT);
return (length > Coder.HEADER_LIMIT ? Coder.LONG_HEADER
: Coder.SHORT_HEADER) + length;
// CHECKSTYLE:ON
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("PMD.NPathComplexity")
public void encode(final SWFEncoder coder, final Context context)
throws IOException {
if (length > Coder.HEADER_LIMIT) {
coder.writeShort((MovieTypes.PLACE_2
<< Coder.LENGTH_FIELD_SIZE) | Coder.IS_EXTENDED);
coder.writeInt(length);
} else {
coder.writeShort((MovieTypes.PLACE_2
<< Coder.LENGTH_FIELD_SIZE) | length);
}
if (Constants.DEBUG) {
coder.mark();
}
context.put(Context.TRANSPARENT, 1);
int bits = 0;
bits |= events.isEmpty() ? 0 : Coder.BIT7;
bits |= depth == null ? 0 : Coder.BIT6;
bits |= name == null ? 0 : Coder.BIT5;
bits |= ratio == null ? 0 : Coder.BIT4;
bits |= colorTransform == null ? 0 : Coder.BIT3;
bits |= transform == null ? 0 : Coder.BIT2;
switch (type) {
case MODIFY:
bits |= Coder.BIT0;
break;
case NEW:
bits |= Coder.BIT1;
break;
default:
bits |= Coder.BIT0;
bits |= Coder.BIT1;
break;
}
coder.writeByte(bits);
coder.writeShort(layer);
if ((type == PlaceType.NEW) || (type == PlaceType.REPLACE)) {
coder.writeShort(identifier);
}
if (transform != null) {
transform.encode(coder, context);
}
if (colorTransform != null) {
colorTransform.encode(coder, context);
}
if (ratio != null) {
coder.writeShort(ratio);
}
if (name != null) {
coder.writeString(name);
}
if (depth != null) {
coder.writeShort(depth);
}
if (!events.isEmpty()) {
int eventMask = 0;
for (final EventHandler handler : events) {
eventMask |= handler.getEventCode();
}
coder.writeShort(0);
if (context.get(Context.VERSION) > STANDARD_EVENTS) {
coder.writeInt(eventMask);
for (final EventHandler handler : events) {
handler.encode(coder, context);
}
coder.writeInt(0);
} else {
coder.writeShort(eventMask);
for (final EventHandler handler : events) {
handler.encode(coder, context);
}
coder.writeShort(0);
}
}
context.remove(Context.TRANSPARENT);
if (Constants.DEBUG) {
coder.check(length);
coder.unmark();
}
}
}