/******************************************************************************* * 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); } } }