package org.activityinfo.ui.client.page;
/*
* #%L
* ActivityInfo Server
* %%
* Copyright (C) 2009 - 2013 UNICEF
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import com.extjs.gxt.ui.client.event.EventType;
import com.extjs.gxt.ui.client.event.Listener;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.activityinfo.legacy.client.AsyncMonitor;
import org.activityinfo.legacy.shared.Log;
import org.activityinfo.ui.client.EventBus;
import org.activityinfo.ui.client.inject.Root;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Coordinates navigation between pages.
* <p/>
* PageManager listens for NavigationEvents, fired either by an individual
* component, or from the HistoryManager, and
*/
@Singleton
public class NavigationHandler {
private final EventBus eventBus;
private final Frame root;
private final Map<PageId, PageLoader> pageLoaders = new HashMap<PageId, PageLoader>();
public static final EventType NAVIGATION_REQUESTED = new EventBus.NamedEventType(
"NavigationRequested");
public static final EventType NAVIGATION_AGREED = new EventBus.NamedEventType(
"NavigationAgreed");
private NavigationAttempt activeNavigation;
@Inject
public NavigationHandler(final EventBus eventBus, final @Root Frame root) {
this.eventBus = eventBus;
this.root = root;
eventBus.addListener(NAVIGATION_REQUESTED,
new Listener<NavigationEvent>() {
@Override
public void handleEvent(NavigationEvent be) {
onNavigationRequested(be);
}
});
Log.debug("PageManager: connected to EventBus and listening.");
Window.addWindowClosingHandler(new Window.ClosingHandler() {
@Override
public void onWindowClosing(Window.ClosingEvent event) {
if (activeNavigation != null && activeNavigation.currentPage != null) {
event.setMessage(activeNavigation.currentPage.beforeWindowCloses());
}
}
});
}
public EventBus getEventBus() {
return eventBus;
}
private void onNavigationRequested(NavigationEvent be) {
if (activeNavigation == null
|| !activeNavigation.getPlace().equals(be.getPlace())) {
activeNavigation = new NavigationAttempt(be.getPlace());
activeNavigation.go();
}
}
public void registerPageLoader(PageId pageId, PageLoader loader) {
pageLoaders.put(pageId, loader);
Log.debug("PageManager: Registered loader for pageId '" + pageId + "'");
}
private PageLoader getPageLoader(PageId pageId) {
// Looks for an ID separator
int index = pageId.toString().indexOf('!');
if (index != -1) {
// Removes the ID from the PageId in order to retrieves the correct
// PageLoader from the map.
pageId = new PageId(pageId.toString().substring(0, index));
}
PageLoader loader = pageLoaders.get(pageId);
if (loader == null) {
Log.debug("PageManager: no loader for " + pageId);
throw new Error("PageManager: no loader for " + pageId);
}
return loader;
}
/**
* Encapsulates a single navigation attempt.
*/
public class NavigationAttempt {
private final PageState place;
private Iterator<PageId> pageHierarchyIt;
private Frame frame;
private Page currentPage;
private PageId targetPage;
private AsyncMonitor loadingPlaceHolder;
public NavigationAttempt(PageState place) {
this.place = place;
}
public PageState getPlace() {
return place;
}
public void go() {
startAtRoot();
confirmPageChange();
}
private void startAtRoot() {
assertViewPathIsNotEmpty();
pageHierarchyIt = place.getEnclosingFrames().iterator();
currentPage = root;
descend();
}
private void descend() {
assertPageIsFrame(currentPage);
frame = (Frame) currentPage;
targetPage = pageHierarchyIt.next();
currentPage = frame.getActivePage();
}
/**
* After each asynchronous call we need to check that the user has not
* requested to navigate elsewhere.
* <p/>
* For example, a page loader may make an asynchronous call, which means
* that an additional JavaScript fragment has to be downloaded from the
* server and parsed before we can continue. During that time, the user
* may have grown tired of waiting and hit the back button or chosen
* another place to go to.
*/
private boolean isStillActive() {
return this == activeNavigation;
}
protected void confirmPageChange() {
if (thereIsNoCurrentPage()) {
proceedWithNavigation();
} else if (targetPageIsAlreadyActive()) {
// ok, no change required.
// descend if necessary
if (hasChildPage()) {
descend();
confirmPageChange();
} else {
proceedWithNavigation();
}
} else {
askPermissionToChange();
}
}
/**
* We need to give the current page an opportunity to cancel the
* navigation. For example, the user may have made changes to the page
* and we don't want to navigate away until we're sure that they can be
* saved.
*/
private void askPermissionToChange() {
currentPage.requestToNavigateAway(place, new NavigationCallback() {
@Override
public void onDecided(boolean allowed) {
if (allowed) {
if (isStillActive()) {
proceedWithNavigation();
}
} else {
Log.debug("Navigation to '" + place.toString()
+ "' refused by " + currentPage.toString());
}
}
});
}
private boolean hasChildPage() {
return pageHierarchyIt.hasNext();
}
private boolean targetPageIsAlreadyActive() {
return currentPage.getPageId().equals(targetPage);
}
private boolean thereIsNoCurrentPage() {
return currentPage == null;
}
private void proceedWithNavigation() {
if (isStillActive()) {
fireAgreedEvent();
startAtRoot();
changePage();
}
}
private void fireAgreedEvent() {
eventBus.fireEvent(new NavigationEvent(NAVIGATION_AGREED, place));
}
protected void changePage() {
/*
* First see if this view is already the active view, in wehich case
* we can just descend in the path
*/
if (!thereIsNoCurrentPage() &&
targetPageIsAlreadyActive() &&
currentPage.navigate(place)) {
changeChildPageIfNecessary();
} else {
shutDownCurrentPageIfThereIsOne();
showPlaceHolder();
schedulePageLoadAfterEventProcessing();
}
}
private void shutDownCurrentPageIfThereIsOne() {
if (!thereIsNoCurrentPage()) {
currentPage.shutdown();
}
}
private void showPlaceHolder() {
loadingPlaceHolder = frame
.showLoadingPlaceHolder(targetPage, place);
}
/**
* Schedules the loadPage() after all UI events in the browser have had
* a chance to run. This assures that the loading placeholder has a
* chance to be added to the page.
*/
private void schedulePageLoadAfterEventProcessing() {
if (GWT.isClient()) {
DeferredCommand.addCommand(new Command() {
@Override
public void execute() {
if (isStillActive()) {
loadPage();
}
}
});
} else {
loadPage();
}
}
/**
* Delegates the creation of the Page component to a registered page
* loader.
*/
private void loadPage() {
PageLoader loader = getPageLoader(targetPage);
loader.load(targetPage, place, new AsyncCallback<Page>() {
@Override
public void onFailure(Throwable caught) {
onPageFailedToLoad(caught);
}
@Override
public void onSuccess(Page page) {
if (isStillActive()) {
onPageLoaded(page);
}
}
});
}
private void onPageFailedToLoad(Throwable caught) {
loadingPlaceHolder.onConnectionProblem();
Log.error("PageManager: could not load page " + targetPage, caught);
}
private void onPageLoaded(Page page) {
makeCurrent(page);
changeChildPageIfNecessary();
}
private void makeCurrent(Page page) {
frame.setActivePage(page);
currentPage = page;
}
private void changeChildPageIfNecessary() {
if (hasChildPage()) {
descend();
changePage();
}
}
private void assertViewPathIsNotEmpty() {
assert place.getEnclosingFrames().size() != 0 : "PageState "
+ place.toString() + " has an empty viewPath!";
}
private void assertPageIsFrame(Page page) {
assert page instanceof Frame : "Cannot load page "
+ pageHierarchyIt.next() + " into " + page.toString()
+ " because " +
page.getClass().getName()
+ " does not implement the PageFrame interface.";
}
}
}