/*
* Copyright 2008-2009 Adam Tacy <adam.tacy AT gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.adamtacy.client.ui.effects;
import java.util.Iterator;
import java.util.Vector;
import org.adamtacy.client.ui.NEffectPanel;
import org.adamtacy.client.ui.effects.events.EffectCompletedEvent;
import org.adamtacy.client.ui.effects.events.EffectCompletedHandler;
import org.adamtacy.client.ui.effects.events.EffectInterruptedEvent;
import org.adamtacy.client.ui.effects.events.EffectInterruptedHandler;
import org.adamtacy.client.ui.effects.events.EffectPausedEvent;
import org.adamtacy.client.ui.effects.events.EffectPausedHandler;
import org.adamtacy.client.ui.effects.events.EffectResumedEvent;
import org.adamtacy.client.ui.effects.events.EffectResumedHandler;
import org.adamtacy.client.ui.effects.events.EffectStartingEvent;
import org.adamtacy.client.ui.effects.events.EffectStartingHandler;
import org.adamtacy.client.ui.effects.events.EffectSteppingEvent;
import org.adamtacy.client.ui.effects.events.EffectSteppingHandler;
import org.adamtacy.client.ui.effects.events.HasAllEffectEventHandlers;
import org.adamtacy.client.ui.effects.impl.browsers.EffectImplementation;
import org.adamtacy.client.ui.effects.transitionsphysics.EaseInOutTransitionPhysics;
import org.adamtacy.client.ui.effects.transitionsphysics.TransitionPhysics;
import com.google.gwt.animation.client.Animation;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Timer;
/**
* A class that provides the basics of an effect.
*
* From v4 a new style of setting properties is introduced.
*
* From v3 of GWT-FX this class is based on the GWT Animation class to keep
* divergence from GWT itself as minimal as possible. To maintain the previous
* API, this class extends Animation with a number of fields and methods.
*
* It is an abstract class that provides a fair amount of functionality, but
* leaves the implementation of the
*
* @author adam
*
*/
public abstract class NEffect extends Animation implements
HasAllEffectEventHandlers {
/**
* Determine if an NEffect has started.
* @return boolean value indicating if NEffect has started or not.
*/
public boolean isStarted() {
return effectStarted;
}
// Indicates if start > end in play parameters (different to inverse)
protected boolean backwards = false;
protected Vector<NEffect> chainedEffects;
protected double currentEffectPosition = 0.0;
protected double currentEffectPhysicalPosition = 0.0;
protected final double DEFAULT_EFFECT_LENGTH = 2.0;
// Removed by enhancement 104 in favour of a Vector effectElements
//protected Element effectElement;
// Enhancement 104
protected Vector<Element> effectElements = new Vector<Element>();
protected boolean effectFinished = false;
protected boolean effectStarted = false;
protected double effectLengthSeconds = DEFAULT_EFFECT_LENGTH;
/**
* Need this as we're not extending a widget here and so have no access to that HandlerManager.
*/
HandlerManager handlerManager = new HandlerManager(DEFAULT_EFFECT_LENGTH);
protected boolean interruptable = true;
protected boolean inverted = false;
/**
* Details if the effect has been initialised yet or not.
* Updated by either the init(NEffectPanel thePanel) method, or through the play() method.
*/
protected boolean isInitialised = false;
// Store the real effect end point - usually this will be at position 1.0
// unless overridden
// by the user (see method interpolate for use of this variable).
double requestedEffectEnd = 1.0;
// Store the real effect start point - usually this will be at position 0.0
// unless overridden
// by the user (see method interpolate for use of this variable).
double requestedEffectStart = 0.0;
Timer startDelay;
/**
* The effectPanel the effect maybe attached to
* From v5 this is nt necessary for all effects.
*/
protected NEffectPanel thePanel;
/**
* Holds the Transition for the effect.
*/
protected TransitionPhysics transition;
public HandlerRegistration addEffectCompletedHandler(
EffectCompletedHandler handler) {
return addHandler(handler, EffectCompletedEvent.getType());
}
public HandlerRegistration addEffectInterruptedHandler(
EffectInterruptedHandler handler) {
return addHandler(handler, EffectInterruptedEvent.getType());
}
public HandlerRegistration addEffectStartingHandler(
EffectStartingHandler handler) {
return addHandler(handler, EffectStartingEvent.getType());
}
public HandlerRegistration addEffectSteppingHandler(
EffectSteppingHandler handler) {
return addHandler(handler, EffectSteppingEvent.getType());
}
public HandlerRegistration addEffectResumedHandler(
EffectResumedHandler handler) {
return addHandler(handler, EffectResumedEvent.getType());
}
public HandlerRegistration addEffectPausedHandler(
EffectPausedHandler handler) {
return addHandler(handler, EffectPausedEvent.getType());
}
/**
* Adds a new Element to be managed by this effect.
* See enhancement 104.
* @param el
*/
public void addEffectElement(Element el){
effectElements.add(el);
}
/**
* Adds this handler to the widget.
*
* @param <H> the type of handler to add
* @param type the event type
* @param handler the handler
* @return {@link HandlerRegistration} used to remove the handler
*/
protected final <H extends EventHandler> HandlerRegistration addHandler(
final H handler, GwtEvent.Type<H> type) {
return ensureHandlers().addHandler(type, handler);
}
/**
* Override the Animate cancel to check that effect can be interrupted.
*/
@Override
public void cancel() {
if (interruptable)
super.cancel();
}
boolean paused = false;
HandlerRegistration chainedEffectsHandlers;
public void chain(final NEffect e) {
if (chainedEffects == null){
chainedEffects = new Vector<NEffect>();
}
chainedEffects.add(e);
// Effects are linked together in the init method.
}
boolean looping = false;
public void setLooping(boolean val) {
looping = val;
}
public boolean isLooped(){
return looping;
}
/**
* Unchain all effects that have previously been chained
*/
public void unchain(){
if(chainedEffects != null){
if(chainedEffectsHandlers!=null)chainedEffectsHandlers.removeHandler();
chainedEffects = null;
chainedEffectsHandlers = null;
}
isInitialised = false;
}
/**
* Ensures the existence of the handler manager.
*
* @return the handler manager
* */
HandlerManager ensureHandlers() {
return handlerManager == null ? handlerManager = new HandlerManager(this)
: handlerManager;
}
/**
* Fires an event. Usually used when passing an event from one source to
* another.
*
* @param event the event
*/
public void fireEvent(GwtEvent<?> event) {
if (handlerManager != null) {
handlerManager.fireEvent(event);
}
}
/**
* Return the effect length in seconds.
*
* @return Effect length in seconds.
*/
public double getDuration() {
return effectLengthSeconds;
}
/**
* Returns first element effect is applied to (after enhancement 104 allowed multiple elements)
* @return
*/
public Element getEffectElement(){
Element toReturn = effectElements.get(0);
if (thePanel!=null) toReturn = thePanel.getElement();
return toReturn;
}
public NEffectPanel getEffectPanel() {
return thePanel;
}
public HandlerManager getHandlers() {
return handlerManager;
}
/**
* Return the current progress of the effect - a value somewhere between 0 and
* 1.
*
* @return
*/
public double getProgress() {
return currentEffectPhysicalPosition;
}
public double getProgressInterpolated() {
return currentEffectPosition;
}
/**
* Return the current Transition Physics Model used by the Effect.
*
* @return
*/
public TransitionPhysics getTransitionPhysics() {
return transition;
}
public void init(){
if (transition == null)
transition = new EaseInOutTransitionPhysics();
if (chainedEffects != null) {
// Start chaining the effects together
// First initialise them all
if(thePanel!=null)
for (Iterator<NEffect> it = chainedEffects.iterator(); it.hasNext();) {
it.next().init(thePanel);
}
// Add an effect completed handler for this effect to fire all chained effects
chainedEffectsHandlers = this.addEffectCompletedHandler(new EffectCompletedHandler() {
public void onEffectCompleted(EffectCompletedEvent event) {
for (Iterator<NEffect> it = chainedEffects.iterator(); it.hasNext();) {
it.next().play();
}
}
});
}
setUpEffect();
}
/**
* Initialises an effect on an effect panel.
*
* @param thePanel The EffectPanel on which effect is being applied.
*/
public void init(NEffectPanel thePanel) {
this.thePanel = thePanel;
init();
isInitialised = true;
}
/**
* Overridden version of the Animate class to allow us to swap in and out the
* transition method. In GWT 1.5 this is a fixed calculation in the Animate
* class.
*
* We also use this method to fire any change events since it is not possible
* elsewhere in the Animate framework, and in theory interpolate would be
* called as many times as the update method.
*/
@Override
protected double interpolate(double progress) {
// Store the physical position (i.e. before interpolation)
currentEffectPhysicalPosition = interpolateWithoutUpdate(progress);
// Interpolate value based on transition physics
double newVal = interpolateFinish(currentEffectPhysicalPosition);
// Right, now we've done the maths, time to fire any change listeners that
// may be registered
// Issue 112 means that EffectSteppingEvents should now be called by override methods.
// fireEvent(new EffectSteppingEvent(newVal,""));
// and finally, return the value.
return newVal;
}
protected double interpolateWithoutUpdate(double progress) {
// Check if effect is set up yet
if (transition == null)
return 1.0;
// Check if the effect is backwards (e.g. requested start is higher than
// requested end)
if (!backwards) {
// It is not, so just find the real progress
progress = progress + requestedEffectStart;
// Check that we are not now past the requested end of effect
if (progress > requestedEffectEnd)
progress = requestedEffectEnd;
} else {
// Effect is backwards, so just rework the progress calculation
progress = requestedEffectStart - progress;
if (progress < requestedEffectEnd)
progress = requestedEffectEnd;
}
if ((progress == 1.0)) {
if (inverted)
return 0.0;
else
return 1.0;
}
if (progress == 0.0) {
if (inverted)
return 1.0;
else
return 0.0;
}
return progress;
}
private double interpolateFinish(double progress){
// Now get the position in animation based on the particular Transition to
// be applied to this effect.
double val = transition.getAnimationPosition(progress);
// Finally, check if the effect is inverted, if so, deal with that.
if (inverted)
val = 1 - val;
return val;
}
/**
*
* Inverts an effect - currently not for Sequential composite effects
*
*/
public void invert() {
assert (!(this instanceof SequentialCompositeEffect));
inverted = !inverted;
}
/**
* Returns a boolean value indicating if the effect has finished or not.
*
* @return True if the effect is finished; False otherwise
*/
public boolean isFinished() {
return effectFinished;
}
/**
* Returns a boolean value to indicate if the effect is interruptable or not.
*
* @return True if the effect is interruptable; false otherwise.
*/
public boolean isInterruptable() {
return interruptable;
}
/**
* Returns a boolean value to indicate if the effect is inverted.
*
* @return
*/
public boolean isInverted() {
return inverted;
}
/**
* Called immediately after the animation is canceled. The default
* implementation of this method calls {@link #onComplete()} only if the
* animation has actually started running.
*
* This overrides the version in the Animate class to allow the inclusion of
* the effectHandlers.
*
*/
@Override
protected void onCancel() {
// Removed call to super, see issues 82 and 83.
// super.onCancel();
if(paused) fireEvent(new EffectPausedEvent());
else fireEvent(new EffectInterruptedEvent());
}
/**
* Called immediately after the animation completes.
*
* This overrides the version in the Animate class to allow the inclusion of
* the effectHandlers.
*/
@Override
protected void onComplete() {
super.onComplete();
effectFinished = true;
fireEvent(new EffectCompletedEvent());
if(looping){
DeferredCommand.addCommand(new Command(){
public void execute() {
play(0.0, 1.0);
}
});
}
}
/**
* Called immediately before the animation starts.
*
* This overrides the version in the Animate class to allow the inclusion of
* the effectHandlers.
*
*/
@Override
protected void onStart() {
// Issue 95 means don't call super.onStart() as it causes any resumeBackwards() calls to flash to position 0.0 first
// super.onStart();
// In GWT1.6 super.onStart() does nothing else but call onUpdate(0.0);
effectFinished = false;
effectStarted = true;
fireEvent(new EffectStartingEvent());
}
/**
* Called when the animation should be updated.
*
* The value of progress is between 0.0 and 1.0 inclusively (unless you
* override the {@link #interpolate(double)} method to provide a wider range
* of values). You can override {@link #onStart()} and {@link #onComplete()}
* to perform setup and tear down procedures.
*
* Note that onUpdate events are now triggered by a call to the interpolate
* method whereas before they were through the direct update methods - this
* needs to be done since the update method in GWT Animation is private.
*
*/
@Override
protected void onUpdate(double progress) {
currentEffectPosition = progress;
}
public void pause(){
this.cancel();
}
/**
* The play method - defaults to playing effect from position 0.0 to 1.0.
* Issue #66 introduced the isInitialised check.
*/
public void play() {
if(!isInitialised){
//setUpEffect();
init();
isInitialised = true;
}
double timeRatio = this.requestedEffectEnd - this.requestedEffectStart;
if (timeRatio<0) timeRatio = -timeRatio;
run((int) ((effectLengthSeconds * 1000) * timeRatio));
}
/**
* The play method - allows specific definition of start and end positions.
*
* Note that the portion of effect selected will run at the same tempo as the
* whole effect, but the firing of any postEffect handlers will not occur
* until the whole duration is complete.
*
* For example, an effect with duration 2 seconds, but calling play(0.0, 0.5)
* will appear to viewer to complete in 1 second, but will not fire any
* postEvents until after 2 seconds.
*
* @param start The requested start position.
* @param end The requested end position.
*/
public void play(double start, double end) {
// If the start point is greater than the end point, then we need to play
// the effect in reverse
// But, note that this is not the same as inverting the effect.
if (start > end)
backwards = true;
else
backwards = false;
// Store the start and end positions.
requestedEffectStart = start;
requestedEffectEnd = end;
// Play the effect as normal
play();
}
public void play(final double start, final double end, int afterDelay){
startDelay = new Timer(){
@Override
public void run() {
play(start, end);
}
};
startDelay.schedule(afterDelay);
}
public void play(int afterDelay){
startDelay = new Timer(){
@Override
public void run() {
play();
}
};
startDelay.schedule(afterDelay);
}
protected void registerEffectElement(){
//if(effectElement==null)effectElement = thePanel.getPanelWidget().getElement();
// Enhancement 104
if(effectElements.isEmpty()){
Element el = thePanel.getWidget().getElement();
effectElements.add(el);
}
}
public void remove() {
cancel();
tearDownEffect();
}
/**
* Allow effect to be removed if it was added directly to Element
* (if added via NEffectPanel it must be removed that way).
*
* See issue #77
*
*/
public void removeEffect(){
removeEffect(true);
}
/**
* Allow effect to be removed if it was added directly to Elements
* It is removed per element.
* (if added via NEffectPanel it must be removed that way).
*
* @param undoEffect set to true to call tearDown which may undo the effect; or false to do nothing
*/
public void removeEffect(boolean undoEffect){
assert(thePanel == null);
if(undoEffect) tearDownEffect();
effectElements = null;
}
/** Reset the effect */
public void reset() {
if (thePanel != null)
onUpdate(0.0);
}
public void resumeBackwards() {
paused = false;
this.play(getProgress(), 0.0);
fireEvent(new EffectResumedEvent());
}
public void resumeBackwards(double end) {
paused = false;
this.play(getProgress(), end);
fireEvent(new EffectResumedEvent());
}
public void resumeForwards() {
paused = false;
this.play(getProgress(), 1.0);
fireEvent(new EffectResumedEvent());
}
public void resumeForwards(double end) {
paused = false;
this.play(getProgress(), end);
fireEvent(new EffectResumedEvent());
}
/**
* Set the effect length.
*
* @param new_EffectLengthSeconds
*/
public void setDuration(double new_EffectLengthSeconds) {
effectLengthSeconds = new_EffectLengthSeconds;
}
/**
* Sets the element for which an effect is applied
* If it is currently applied to any elements, then those elements are removed from the effect after
* enhancement 104 allowed multiple elements to be managed.
* See addEffectElement() if you wish to add elements to be managed by this effect
* @param newEl
*/
public void setEffectElement(Element newEl){
if (thePanel!=null)throw new RuntimeException("Trying to change effectElement when an EffectPanel is in place");
effectElements = new Vector<Element>();
effectElements.add(newEl);
isInitialised = false;
}
/**
* Allow external user to specify position within an effect
*
* @param progress the position to be set.
*/
public void setPosition(double progress) {
if (!this.isInitialised) {
init();
isInitialised = true;
}
double val = interpolate(progress);
onUpdate(val);
if (val == 0) fireEvent(new EffectStartingEvent());
else if (val == 0) fireEvent(new EffectCompletedEvent());
else fireEvent(new EffectSteppingEvent());
}
/*
*
*/
public void setTransitionType(TransitionPhysics newTransition) {
this.transition = newTransition;
if(chainedEffects!=null){
for (Iterator<NEffect> it = chainedEffects.iterator(); it.hasNext();) {
NEffect theEffect = it.next();
if(!theEffect.equals(this))theEffect.setTransitionType(newTransition);
}
}
}
/**
* Gets the layout definition that IE requires to use filters.
* Only has meaning for IE.
*/
public String getIELayoutDefinition(){
return EffectImplementation.getIELayoutDefinition();
}
/**
* Sets the layout definition that IE requires to use filters.
* Is only required for IE.
*/
public void setIELayoutDefinition(String id, String val){
EffectImplementation.setIELayoutDefinition(id,val);
}
/**
* Method that is called to set the effect up. It is abstract here since we
* are not defining an actual effect; but it is expected to be implemented by
* implementations. (For example a Show effect may wish to have this method
* hide the component before starting).
*/
public abstract void setUpEffect();
/**
* Method that is called when the effect is removed from a panel. Usually this
* will not do anything, but for sure the Move effect needs to fix the style
* properties that it sets up.
*/
public abstract void tearDownEffect();
}