/* * Copyright 2010 Daniel Kurka * * 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 com.googlecode.mgwt.mvp.client; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.Set; import com.google.gwt.activity.shared.AbstractActivity; import com.google.gwt.activity.shared.Activity; import com.google.gwt.activity.shared.ActivityManager; import com.google.gwt.activity.shared.ActivityMapper; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.event.shared.ResettableEventBus; import com.google.gwt.event.shared.UmbrellaException; import com.google.gwt.place.shared.Place; import com.google.gwt.place.shared.PlaceChangeEvent; import com.google.gwt.place.shared.PlaceChangeRequestEvent; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.google.gwt.user.client.ui.IsWidget; import com.google.web.bindery.event.shared.EventBus; import com.googlecode.mgwt.ui.client.widget.animation.AnimatableDisplay; import com.googlecode.mgwt.ui.client.widget.animation.Animation; import com.googlecode.mgwt.ui.client.widget.animation.AnimationEndCallback; /** * This is a fork of @link {@link ActivityManager} that has the same features, * but also adds animations to the lifecycle of Activities. * * It can be used as a replacement for {@link ActivityManager}, but requires an * instance of {@link AnimationMapper} to work properly * * @author Daniel Kurka */ public class AnimatingActivityManager implements PlaceChangeEvent.Handler, PlaceChangeRequestEvent.Handler { /** * Wraps our real display to prevent an Activity from taking it over if it * is not the currentActivity. */ private class ProtectedDisplay implements AcceptsOneWidget { private final Activity activity; public ProtectedDisplay(Activity activity) { this.activity = activity; } public void setWidget(IsWidget view) { if (this.activity == AnimatingActivityManager.this.currentActivity) { startingNext = false; showWidget(view, currentIsFirst); } else { if (this.activity == AnimatingActivityManager.this.lastActivity) { showWidget(view, currentIsFirst); } } } } private static final Activity NULL_ACTIVITY = new AbstractActivity() { @Override public void start(AcceptsOneWidget panel, com.google.gwt.event.shared.EventBus eventBus) { panel.setWidget(null); } }; private final ActivityMapper mapper; private final EventBus eventBus; private final ResettableEventBus stopperedEventBus; private Activity currentActivity = NULL_ACTIVITY; private Place currentPlace; private boolean currentIsFirst = false; private Activity lastActivity = NULL_ACTIVITY; private AnimatableDisplay display; private boolean startingNext = false; private HandlerRegistration handlerRegistration; private final AnimationMapper animationMapper; private boolean listeningForAnimationEnd = false; private boolean ignorePlaceChange = false; private boolean fireAnimationEvents; private LinkedList<PlaceChangeEvent> placeChangeStack = new LinkedList<PlaceChangeEvent>(); /** * Create an ActivityManager. Next call {@link #setDisplay}. * * @param mapper finds the {@link Activity} for a given * {@link com.google.gwt.place.shared.Place} * @param eventBus source of {@link PlaceChangeEvent} and * {@link PlaceChangeRequestEvent} events. * @param animationMapper a * {@link com.googlecode.mgwt.mvp.client.AnimationMapper} object. */ public AnimatingActivityManager(ActivityMapper mapper, AnimationMapper animationMapper, EventBus eventBus) { this.mapper = mapper; this.animationMapper = animationMapper; this.eventBus = eventBus; this.stopperedEventBus = new ResettableEventBus(eventBus); } /** * {@inheritDoc} * * Deactivate the current activity, find the next one from our * ActivityMapper, and start it. * <p> * The current activity's widget will be hidden immediately, which can cause * flicker if the next activity provides its widget asynchronously. That can * be minimized by decent caching. Perenially slow activities might mitigate * this by providing a widget immediately, with some kind of "loading" * treatment. * * @see com.google.gwt.place.shared.PlaceChangeEvent.Handler#onPlaceChange(PlaceChangeEvent) */ public void onPlaceChange(PlaceChangeEvent event) { if (ignorePlaceChange) { //remember the place change event to be executed after the current one is done placeChangeStack.add(event); return; } Activity nextActivity = getNextActivity(event); Animation animation = getAnimation(event); Throwable caughtOnStop = null; Throwable caughtOnStart = null; if (nextActivity == null) { nextActivity = NULL_ACTIVITY; } if (currentActivity.equals(nextActivity)) { return; } if (startingNext) { // The place changed again before the new current activity showed // its // widget currentActivity.onCancel(); currentActivity = NULL_ACTIVITY; startingNext = false; } else { /* * Kill off the activity's handlers, so it doesn't have to worry * about them accidentally firing as a side effect of its tear down */ stopperedEventBus.removeHandlers(); try { currentActivity.onStop(); } catch (Throwable t) { caughtOnStop = t; } finally { /* * And kill them off again in case it was naughty and added new * ones during onstop */ stopperedEventBus.removeHandlers(); } } currentActivity = nextActivity; currentPlace = event.getNewPlace(); if (animation != null) { currentIsFirst = !currentIsFirst; } startingNext = true; /* * Now start the thing. Wrap the actual display with a per-call instance * that protects the display from canceled or stopped activities, and * which maintains our startingNext state. */ try { currentActivity.start(new ProtectedDisplay(currentActivity), stopperedEventBus); } catch (Throwable t) { caughtOnStart = t; } if (caughtOnStart != null || caughtOnStop != null) { Set<Throwable> causes = new LinkedHashSet<Throwable>(); if (caughtOnStop != null) { causes.add(caughtOnStop); } if (caughtOnStart != null) { causes.add(caughtOnStart); } throw new UmbrellaException(causes); } // animate animate(animation); } public void setFireAnimationEvents(boolean fireAnimationEvents) { this.fireAnimationEvents = fireAnimationEvents; } public boolean isFireAnimationEvents() { return fireAnimationEvents; } private void onAnimationEnd() { if (listeningForAnimationEnd) { if (fireAnimationEvents) { eventBus.fireEvent(new MGWTAnimationEndEvent()); } if (!placeChangeStack.isEmpty()) { //execute deffered so that animation end will fire! Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { ignorePlaceChange = false; PlaceChangeEvent poll = placeChangeStack.poll(); onPlaceChange(poll); } }); } else { ignorePlaceChange = false; } } } private void animate(Animation animation) { listeningForAnimationEnd = true; ignorePlaceChange = true; if (fireAnimationEvents) { eventBus.fireEvent(new MGWTAnimationStartEvent()); } if (animation == null) { AnimatingActivityManager.this.onAnimationEnd(); return; } display.animate(animation, currentIsFirst, new AnimationEndCallback() { @Override public void onAnimationEnd() { AnimatingActivityManager.this.onAnimationEnd(); } }); } private Animation getAnimation(PlaceChangeEvent event) { Place newPlace = event.getNewPlace(); return animationMapper.getAnimation(currentPlace, newPlace); } public void onPlaceChangeRequest(PlaceChangeRequestEvent event) { if (!currentActivity.equals(NULL_ACTIVITY)) { event.setWarning(currentActivity.mayStop()); } } /** * Sets the display for the receiver, and has the side effect of starting or * stopping its monitoring the event bus for place change events. * <p> * If you are disposing of an ActivityManager, it is important to call * setDisplay(null) to get it to deregister from the event bus, so that it * can be garbage collected. * * @param display an instance of AcceptsOneWidget */ public void setDisplay(AnimatableDisplay display) { boolean wasActive = (null != this.display); boolean willBeActive = (null != display); this.display = display; if (wasActive != willBeActive) { updateHandlers(willBeActive); } } private Activity getNextActivity(PlaceChangeEvent event) { if (display == null) { /* * Display may have been nulled during PlaceChangeEvent dispatch. * Don't bother the mapper, just return a null to ensure we shut * down the current activity */ return null; } return mapper.getActivity(event.getNewPlace()); } private void showWidget(IsWidget view, boolean first) { if (display != null) { if (first) { display.setFirstWidget(view); } else { display.setSecondWidget(view); } } } private void updateHandlers(boolean activate) { if (activate) { final com.google.web.bindery.event.shared.HandlerRegistration placeReg = eventBus.addHandler(PlaceChangeEvent.TYPE, this); final com.google.web.bindery.event.shared.HandlerRegistration placeRequestReg = eventBus.addHandler(PlaceChangeRequestEvent.TYPE, this); this.handlerRegistration = new HandlerRegistration() { public void removeHandler() { placeReg.removeHandler(); placeRequestReg.removeHandler(); } }; } else { if (handlerRegistration != null) { handlerRegistration.removeHandler(); handlerRegistration = null; } } } }