/*******************************************************************************
* Copyright (c) 2010-2015 Henshin developers. All rights reserved.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* TU Berlin, University of Luxembourg, SES S.A.
*******************************************************************************/
package de.tub.tfs.muvitor.animation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.LayoutListener;
import org.eclipse.draw2d.Polyline;
import org.eclipse.draw2d.ScalableFreeformLayeredPane;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gef.EditPartViewer;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.GraphicalViewer;
import org.eclipse.gef.LayerConstants;
import org.eclipse.gef.editparts.LayerManager;
import org.eclipse.gef.editparts.ScalableFreeformRootEditPart;
import org.eclipse.gef.ui.parts.ScrollingGraphicalViewer;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.IPage;
import org.eclipse.ui.part.IPageBookViewPage;
import org.eclipse.ui.part.PageBookView;
/**
* This class represents a (model) element to be animated in some viewer which
* is determined by a context parent element. It manages its {@link Localizer}s
* and preparation of animation. AnimatedElements respond to the flag
* {@link AnimatingCommand#isDebug()} by drawing a line that shows the path of
* the animated figure.
*
* <p>
* This class is not intended for direct usage! It will be used properly by
* {@link AnimatingCommand}s.
* </p>
*
* <p>
* The animating command calls {@link #prepareForAnimation(boolean)} for all
* animated elements. When performing a step, the animating command calls
* {@link #prepareStep(int)} for all involved animated elements before running
* the animation. Eventually, all animated elements are being reset by calling
* {@link #animationDone()}.
* </p>
*
* @author Tony Modica
*
*/
final class AnimatedElement extends LayoutListener.Stub {
/**
* A map for general allowing {@link MultipleAnimator} to access data for
* the animatedFigure on the observed pane.
*/
final static private Map<IFigure, AnimatedElement> paneAnimatedElementMap = new HashMap<IFigure, AnimatedElement>();
/**
* This method is used to find the {@link GraphicalViewer} having the passed
* {@link #topModel} as contents. This viewer must been hosted in an
* {@link IPageBookViewPage} that implements
* {@link IGraphicalViewerProvider}.
*
* <p>
* Note: This method looks <i>only</i> for GraphicalViewers in pages of
* {@link PageBookView}s! It does not look for viewers in editors (for now)!
* </p>
*
* @param topModel
* The model whose hosting viewer is been looked for
* @return The unique viewer having the model as its contents.
* <code>null</code> if not such a unique viewer exists.
*
* @see IGraphicalViewerProvider
*/
public static GraphicalViewer findViewerShowing(final EObject topModel) {
final ArrayList<GraphicalViewer> viewers = new ArrayList<GraphicalViewer>();
final IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getActivePage();
final IViewReference[] viewRefs = activePage.getViewReferences();
for (final IViewReference viewRef : viewRefs) {
final IViewPart view = viewRef.getView(false);
if (view instanceof PageBookView) {
final IPage page = ((PageBookView) view).getCurrentPage();
if (page instanceof IGraphicalViewerProvider) {
final GraphicalViewer viewer = ((IGraphicalViewerProvider) page)
.getViewer(topModel);
if (viewer != null) {
viewers.add(viewer);
}
}
}
}
// return viewer only if it is the only one showing the model
if (viewers.size() == 1) {
return viewers.get(0);
}
return null;
}
private boolean canPerform = false;
private double currentSizeFactor;
private Polyline debugLine;
private Dimension figureCenterOffset;
private IFigure layer;
// initialization for first step to perform
private double nextSizeFactor = 1.0;
private Point originalLocation;
private IFigure originalParent;
private int originalPosition;
private ScalableFreeformLayeredPane pane;
/**
* An ArrayList of {@link Localizer}s defining the path of this animated
* element.
*/
private final ArrayList<Localizer> path = new ArrayList<Localizer>();
/**
* A path modifier that will be used for calculating the coordinates the
* figure to be animated should follow.
*/
private final AnimationPathModifier pathModifier;
/*******************************************************************************
* The following is a special modification of org.eclipse.draw2d.Animator to
* be instantiated for different viewers and supporting
* {@link AnimationPathModifier}s and resizing on an exclusive pane for this
* AnimatedElement.
*/
private float progress = -1;
/**
* This holds the position of the first Localizer not being a place holder,
* i.e. the real starting location.
*/
private int startIndex;
/**
* A container model being an (indirect) parent of {@link #animatedModel}.
* The viewer having this container as contents (determined by
* {@link #findViewerShowing(EObject)}) will be used for animation .
*/
final private EObject topModel;
/**
* transient data fields to perform the animation, will be set null in
* {@link #animationDone()}
*/
private GraphicalViewer viewer;
/**
* The resolved figure in the determined viewer for the model. Or a custom
* figure to be animated, so that there is no need for a resolved figure.
*/
IFigure animatedFigure;
/**
* The model element whose figure in a specific viewer is to be animated (if
* no figure to be animated is explicitly defined).
*/
final EObject animatedModel;
Point finalLocation;
/**
* {@link #animationDone()} will store these locations after animation. They
* can be retrieved with {@link AnimatingCommand#getFinalLocation(Object)}
* and {@link AnimatingCommand#getInitalLocation(Object)}.
*/
Point initialLocation;
/**
* Universal constructor.
*
* @param modelOrEditPartOrFigure
* can be an {@link GraphicalEditPart} or {@link EObject}
* determining the model whose figure should be animated.
* Alternatively, you may pass a specific {@link IFigure} to be
* animated.
* @param viewerOrContents
* can be an {@link EObject} or {@link EditPartViewer}
* determining the viewer holding a figure of the model
* @param pathModifier
* optional {@link AnimationPathModifier}
*/
AnimatedElement(final Object modelOrEditPartOrFigure, final Object viewerOrContents,
final AnimationPathModifier pathModifier) {
// determine model to be animated
if (modelOrEditPartOrFigure instanceof GraphicalEditPart) {
animatedModel = (EObject) ((GraphicalEditPart) modelOrEditPartOrFigure).getModel();
} else if (modelOrEditPartOrFigure instanceof EObject) {
animatedModel = (EObject) modelOrEditPartOrFigure;
} else if (modelOrEditPartOrFigure instanceof IFigure) {
animatedModel = null;
animatedFigure = (IFigure) modelOrEditPartOrFigure;
} else {
throw new IllegalArgumentException(
"AnimatedElement must be initialized with an EObject, GraphicalEditPart, or IFigure!");
}
// determine context topModel
if (viewerOrContents instanceof EObject) {
topModel = (EObject) viewerOrContents;
} else if (viewerOrContents instanceof EditPartViewer) {
final ScrollingGraphicalViewer contentsViewer = (ScrollingGraphicalViewer) viewerOrContents;
topModel = (EObject) contentsViewer.getContents().getModel();
} else {
throw new IllegalArgumentException(
"AnimatedElement must be initialized with a top-level EObject or EditPartViewer!");
}
// select standard path modifier if none has been specified
if (pathModifier == null) {
this.pathModifier = AnimationPathModifier.getStandardModifier();
} else {
this.pathModifier = pathModifier;
}
startIndex = -1;
}
/**
* Hooks invalidation in case animation is in progress.
*
* @see LayoutListener#invalidate(IFigure)
*/
@Override
final public void invalidate(final IFigure container) {
if (MultipleAnimation.isInitialRecording()) {
MultipleAnimation.hookPane((ScalableFreeformLayeredPane) container);
}
}
/**
* Hooks layout in case animation is in progress.
*
* @see LayoutListener#layout(org.eclipse.draw2d.IFigure)
*/
@Override
public final boolean layout(final IFigure container) {
// hook playback
if (MultipleAnimation.isAnimating() && MultipleAnimation.toCapture.contains(container)) {
return playback(container);
}
return false;
}
/**
* Hooks post layout in case animation is in progress. This was used in the
* original GEF animation, but now we call hookNeedsCapture manually for the
* AnimatedElement. This method is put here for documentation only.
*
* @see LayoutListener#postLayout(IFigure)
*/
@Override
final public void postLayout(final IFigure container) {
// if (MultipleAnimation.isFinalRecording()) {
// MultipleAnimation.hookNeedsCapture(container);
// }
}
@Override
final public String toString() {
final StringBuilder buffer = new StringBuilder();
buffer.append("AnimatedElement: ");
if (animatedModel != null) {
buffer.append(animatedModel);
} else {
buffer.append(animatedFigure);
}
return buffer.toString();
}
/**
* Called by {@link #prepareLocalizers()}. Interpolates the
* {@link Localizer}s in the {@link #path} that need interpolation by
* calculating a delta for {@link Localizer}s between sufficiently specified
* {@link Localizer}s. This results in a linear interpolation of locations
* and sizes.
*
* @param isForLocation
* determined whether locations or sizes of the {@link Localizer}
* {@link #path} should be interpolated.
*/
final private void performInterpolation(final boolean isForLocation) {
/*
* interpolate (after the first defined Localizer on) where needed
*/
int i = startIndex + 1;
interpolate: while (i < path.size() - 1) {
Localizer currentLocalizer = path.get(i);
if (!currentLocalizer.needsInterpolation(isForLocation)) {
// location/size is already resolved
i++;
continue interpolate;
}
// find next non-interpolated Localizer
int nextDefLocIndex = -1;
for (int j = i + 1; j <= path.size() - 1; j++) {
if (!path.get(j).needsInterpolation(isForLocation)) {
nextDefLocIndex = j;
break;
}
}
// base is the previous (resolved) localizer
Localizer base = path.get(i - 1);
// if i is beyond the last non-interpolated location/sizeFactor just
// copy the previous value to prevent any animation
if (nextDefLocIndex == -1) {
if (isForLocation) {
currentLocalizer.resolvedLocation = base.resolvedLocation.getCopy();
} else {
currentLocalizer.sizeFactor = base.sizeFactor;
}
continue interpolate;
}
// build interpolation delta data for this range of Localizers
Dimension locDelta = null;
double sizeDelta = 0;
if (isForLocation) {
final Point start = base.resolvedLocation;
final Point end = path.get(nextDefLocIndex).resolvedLocation;
final double scale = 1.0 / (nextDefLocIndex - (i - 1));
locDelta = end.getDifference(start).scale(scale);
} else {
final double start = base.sizeFactor;
final double end = path.get(nextDefLocIndex).sizeFactor;
sizeDelta = (end - start) / (nextDefLocIndex - (i - 1));
}
/*
* optimization: reusing the delta till the next non-interpolated
* Localizer is reached
*/
while (i < nextDefLocIndex) {
if (isForLocation) {
currentLocalizer.resolvedLocation = base.resolvedLocation
.getTranslated(locDelta);
} else {
currentLocalizer.sizeFactor = base.sizeFactor + sizeDelta;
}
// proceed to next possibly to be interpolated Localizer
base = currentLocalizer;
i++;
currentLocalizer = path.get(i);
}
}
}
/**
* Plays back the animated layout. Called when container is being layouted
* and MultipleAnimation in PLAYBACK state.
*
*/
final private boolean playback(final IFigure container) {
/*
* fix: prevent revalidating loop if playback would cause an
* invalidation loop, i.e. for connections connected to the animated
* nodes: test if the current animation's "progress state" has already
* been played back
*/
if (progress == MultipleAnimation.progress) {
return false;
}
progress = MultipleAnimation.progress;
// get initial states for container
final Map<IFigure, Rectangle> initial = MultipleAnimation.initialStates.get(container);
if (initial == null) {
return false;
}
// get final states for container
final Map<IFigure, Rectangle> ending = MultipleAnimation.finalStates.get(container);
/*
* Iterating over the figures that are explicitly set as animated as
* AnimatedElements should give more performance. Improved: we have
* exactly one figure on each pane! See prepareForAnimation for this!
*/
final Rectangle initialBounds = initial.get(animatedFigure);
final Rectangle endingBounds = ending.get(animatedFigure);
// save creation of one rectangle by using the singleton instance
// temporarily
// figures just copy the int values when setting new bounds
final Rectangle newBounds = Rectangle.SINGLETON;
newBounds.setSize(initialBounds.width, initialBounds.height);
final Point newLocation = pathModifier.getLocation(initialBounds, endingBounds, progress);
// when debugging, we add points to a polyline showing the path
if (AnimatingCommand.isDebug()) {
// debug moving figures that got associated a polyline debug
// figure
if (debugLine != null) {
newBounds.setLocation(newLocation);
// // show some debugging information
// System.out.println("Point(" + newBounds.x + ","
// + newBounds.y + ") progress:" + progress);
debugLine.addPoint(newBounds.getCenter());
}
}
final double newPaneScale = currentSizeFactor + (nextSizeFactor - currentSizeFactor)
* progress;
pane.setScale(newPaneScale);
final double reziLocationFactor = currentSizeFactor / newPaneScale;
newLocation.translate(-figureCenterOffset.width, -figureCenterOffset.height);
newLocation.scale(reziLocationFactor);
newLocation.translate(figureCenterOffset);
newBounds.setLocation(newLocation);
animatedFigure.setBounds(newBounds);
return true;
}
/**
* Called by {@link #prepareForAnimation(boolean)} to resolve and to cache
* the first not interpolated {@link Localizer} (for the case that its model
* is targeted by some Localizer). After that the rest of the
* {@link Localizer} {@link #path} is resolved to determine their
* {@link Localizer#needsInterpolation(boolean)} status. These are
* interpolated (if needed) by calling
* {@link #performInterpolation(boolean)}.
*/
final private void prepareLocalizers() {
/*
* try to resolve first location and check if interpolation is possible
* at all
*/
if (path.get(startIndex).resolveLocation(viewer, animatedModel) == null) {
throw new IllegalArgumentException(
"Start Localizer can not be found or needs (partial) interpolation in this animated element: "
+ toString());
}
/*
* try to resolve locations and sizes in Localizers as far as possible
*/
for (int i = startIndex; i < path.size(); i++) {
path.get(i).resolveLocation(viewer, animatedModel);
}
// interpolate locations and size factors independently, but size
// factors first!
performInterpolation(false);
performInterpolation(true);
/*
* TODO add annotation Figure (with isStatic) (with Pointer or
* Connection) data to Localizer
*/
/*
* TODO add glowing Figure data to Localizer and interpolate brightness
* to target color
*/
/*
* TODO generalize interpolation mechanism, but the location is the most
* important value that must be defined for "important" localizers!
* Glow, size change, and annotation are considered optional
*/
}
/**
* Adds a {@link Localizer} to the path which will be completely
* interpolated.
*/
final void addPlaceholderStep() {
path.add(new Localizer(null, -1));
}
/**
* Convenience method. Use carefully with size changes!
*
* Adds a {@link Localizer}, relying on the passed object, to the path of
* this animated element. locationObject is used to resolve its location in
* a viewer context. If locationObject is not null the size factor is fixed
* to 1, avoiding size change. Otherwise the size factor will be set to -1
* to force interpolation.
*
* @param locationObject
* If <code>null</code>, the added {@link Localizer} will be
* interpolated completely.
*
* @see Localizer#Localizer(Object)
*/
final void addStep(final Object locationObject) {
if (locationObject == null) {
// add place holder to be interpolated
addPlaceholderStep();
} else {
addStep(locationObject, 1);
}
}
/**
* Adds a {@link Localizer} relying on the passed objects to the path of
* this animated element. These objects are used to resolve a location and a
* size in a viewer context. Look at the {@link Localizer} constructors for
* details.
*
* @param locationObject
* @param sizeFactor
*
* @see Localizer#Localizer(Object, double)
*/
final void addStep(final Object locationObject, final double sizeFactor) {
// remember first non-interpolated localizer
if (startIndex == -1 && locationObject != null) {
startIndex = path.size();
}
path.add(new Localizer(locationObject, sizeFactor));
}
/**
* Called by {@link AnimatingCommand} after all steps of an animation have
* been performed. Sets the animated figure's original parent and removes
* the animated pane (and this LayoutListener on it). The used viewer will
* be added to {@link AnimatingCommand#usedViewers}. All temporary data only
* needed for the current animation will be deleted.
*/
final void animationDone() {
if (canPerform) {
// reset viewer and remember this
pane.removeLayoutListener(this);
paneAnimatedElementMap.remove(pane);
layer.remove(pane);
// restore original parent if the model's figure has been used
if (originalParent != null) {
originalParent.add(animatedFigure, originalPosition);
animatedFigure.setLocation(originalLocation);
originalParent = null;
originalLocation = null;
}
AnimatingCommand.usedViewers.add(viewer);
pane = null;
canPerform = false;
}
// discard the animatedFigure if the it has been found for a model
// rather than been specified
if (animatedModel != null) {
animatedFigure = null;
}
// discard temporary resolvedLocations in Localizer path but we keep
// initial and final location!
for (final Localizer localizer : path) {
localizer.resolvedLocation = null;
}
figureCenterOffset = null;
viewer = null;
debugLine = null;
layer = null;
}
/**
* Called by {@link AnimatingCommand}, initializes the animation by doing
* the following:
*
* <ul>
* <li>determine the viewer having the {@link #topModel} as contents
* <li>get the figure representing the {@link #animatedModel} in this viewer
* <li>create an extra layer for this particular figure and move the figure
* to it
* <li>register this as LayoutListener to the new layer. This will do the
* job that Animator does in GEF.
* <li>call {@link #prepareLocalizers()}
* <li>check if this figure can be animated at all
* <li>set the figure' bounds to the initial (or final for undo) position
* </ul>
*
* @param isUndo
* set <code>true</code> if the element should be prepared for
* backwards undo animation
* @param doFlush
* if <code>true</code> the viewer will be flushed before
* animation
*
* @see #findViewerShowing(EObject)
*/
final void prepareForAnimation(final boolean isUndo, final boolean doFlush) {
viewer = findViewerShowing(topModel);
// short test if an animation is possible at all
if (viewer == null || startIndex == -1 || !viewer.getControl().isVisible()) {
canPerform = false;
return;
}
// to remove disturbing handles: set empty selection
viewer.setSelection(StructuredSelection.EMPTY);
/*
* there may be a new figure that has just been created, flush the
* viewer to layout it correctly first! this is in the responsibility of
* the user!
*/
if (doFlush) {
viewer.flush();
}
// prepare and check localizers
prepareLocalizers();
/*
* ensure that this can only be animated if the localizers (after
* startIndex) have been sufficiently resolved/interpolated
*/
for (int i = startIndex; i < path.size(); i++) {
final Localizer localizer = path.get(i);
if (localizer.needsInterpolation(true) || localizer.needsInterpolation(false)) {
canPerform = false;
return;
}
}
/*
* retrieve a figure from the viewer whenever a model has been specified
* so the animated figure can still be accessed after animation
*/
if (animatedModel != null) {
final GraphicalEditPart editPart = (GraphicalEditPart) viewer.getEditPartRegistry()
.get(animatedModel);
if (editPart == null) {
canPerform = false;
return;
}
// the figure of the passed model has to be animated
animatedFigure = editPart.getFigure();
// store original parent of the model's figure
originalParent = animatedFigure.getParent();
originalPosition = originalParent.getChildren().indexOf(animatedFigure);
originalLocation = animatedFigure.getBounds().getLocation();
}
figureCenterOffset = animatedFigure.getSize().scale(-0.5);
// System.out.println("figureCenterOffset : " + figureCenterOffset);
// store initial and final locations of the animated figure
initialLocation = path.get(startIndex).resolvedLocation;
if (initialLocation != null) {
initialLocation.getTranslated(figureCenterOffset);
}
finalLocation = path.get(path.size() - 1).resolvedLocation;
if (finalLocation != null) {
finalLocation.getTranslated(figureCenterOffset);
}
/*
* get the ScalableFreeformLayeredPane of the
* ScalableFreeformRootEditPart to put the animated figure on it
*/
final ScalableFreeformRootEditPart root = (ScalableFreeformRootEditPart) viewer
.getRootEditPart();
layer = LayerManager.Helper.find(root).getLayer(LayerConstants.SCALABLE_LAYERS);
/*
* install sub-ScalableLayeredPanes on the pane to support scale
* manipulation of each animated element
*/
pane = new ScalableFreeformLayeredPane();
layer.add(pane);
pane.add(animatedFigure);
pane.validate();
pane.addLayoutListener(this);
// let the MultipleAnimator know which AnimatedElement is
// responsible for this pane (and to access data)
paneAnimatedElementMap.put(pane, this);
// draw optional debug path line
if (AnimatingCommand.isDebug()) {
// when debugging show the path as a polyline
debugLine = new Polyline();
debugLine.setForegroundColor(ColorConstants.red);
// use rectangle centers for the debugLine
if (isUndo) {
debugLine.setStart(finalLocation);
} else {
debugLine.setStart(initialLocation);
}
// FIXED: I don't know why, but the line has to been added *before*
// the animated figure. Does not matter any more, as I use an extra
// pane for each animated figure
layer.add(debugLine);
}
canPerform = true;
}
/**
* Called by {@link AnimatingCommand}. Revalidates the animated layer if
* needed, sets the new bounds to the animated figure, and signals an
* animation to be performed on this layer.
*
* @param i
* the step in the {@link #path} is going to be performed by
* {@link AnimatingCommand}
*/
final void prepareStep(final int i) {
// don't animate before the first (fully) specified localizer
if (canPerform && i >= startIndex) {
/*
* The pane is marked only once (for performance) as invalid in its
* UpdateManager. Because we hooked the MultipleAnimator into the
* layer, both will be hooked in the MultipleAnimation as well (see
* MultipleAnimator.invalidate(IFigure))
*/
pane.revalidate();
/*
* This is a fix of the original GEF animation mechanism, so that
* Animation.doRun() does not need to validate (which presumably
* calls hookNeedCapture via Animator's postLayout) which would
* layout the figure to its old position!
*/
MultipleAnimation.hookNeedsCapture(pane);
final Localizer currentLocalizer = path.get(i);
currentSizeFactor = nextSizeFactor;
nextSizeFactor = currentLocalizer.sizeFactor;
// reuse singleton point to save creation of an object
final Point newLocation = Point.SINGLETON;
newLocation.setLocation(currentLocalizer.resolvedLocation);
// System.out.print("Localizer - Target Center:" + newLocation);
/*
* translate location to match animated figure's center. This must
* be done here instead of in MultipleAnimator so that no extra
* moving animation is done because of the difference between
* Localizer and center.
*/
newLocation.scale(1 / currentSizeFactor).translate(figureCenterOffset);
// System.out.println(" Target location for animated figure:"
// + newLocation);
/*
* set just the target center location here, size calculation will
* be done by the LayoutListener parts.
*/
animatedFigure.setLocation(newLocation);
}
}
}