package org.vaadin.touchkit.ui;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Stack;
import org.vaadin.touchkit.gwt.client.vcom.navigation.NavigationManagerSharedState;
import org.vaadin.touchkit.ui.NavigationManager.NavigationEvent.Direction;
import com.vaadin.event.ConnectorEventListener;
import com.vaadin.ui.AbstractComponentContainer;
import com.vaadin.ui.Component;
import com.vaadin.util.ReflectTools;
/**
* The NavigationManager is a non-visible component container that allows for
* smooth navigation between components, or views. It support all components,
* but back buttons are updated automatically only for {@link NavigationView}s.
* <p>
* When a component is navigated to, it replaces the currently visible
* component, which in turn is pushed on to the stack of previous views. One can
* navigate backwards by calling {@link #navigateBack()}, in which case the
* currently visible view is forgotten (still cached in case the user decides to
* navigate to it again, see {@link #getNextComponent()}) and the previous view
* is restored from the stack and made visible.
* <p>
* When used with {@link NavigationView}s, {@link NavigationBar}s and
* {@link NavigationButton}s, navigation is smooth and quite automatic.
* <p>
* Bootstrap the navigation by giving the {@link NavigationManager} an initial
* view, either by using the constructor {@link #NavigationManager(Component)}
* or by calling {@link #navigateTo(Component)}.
*/
public class NavigationManager extends AbstractComponentContainer {
/*
* Implementation notes
*
* Actually has three 'active' components: previous, current and next. The
* previous component is actually pushed onto the viewStack only when
* navigateTo() pushes everything down. I.e setPreviousComponent() actually
* replaces the previous component before it's pushed onto the stack. In
* javadoc, this is simplified to ignore implementation details, instead
* pretending the previous component is topmost on the 'history'.
*/
private Stack<Component> viewStack = new Stack<Component>();
private boolean maintainBreadcrumb = true;
/**
* Constructs a NavigationManager that is 100% wide and high.
*/
public NavigationManager() {
setSizeFull();
}
/**
* Constructs a NavigationManager that is 100% wide and high, and initially
* navigates to (shows) the given component.
*/
public NavigationManager(Component c) {
this();
navigateTo(c);
}
@Override
public NavigationManagerSharedState getState() {
return (NavigationManagerSharedState) super.getState();
}
/**
* Gets the view stack. Each time the user navigates forward, the previous
* view is pushed on top of the view stack and when navigating backwards,
* views are popped off of the view stack.
* <p>
* Developers can override components in this stack if they want to manually
* modify the breadcrumb or e.g. release previous views for garbage
* collection.
*
* @see #isMaintainBreadcrumb()
*
* @return the navigation view stack.
*/
public Stack<Component> getViewStack() {
return viewStack;
}
/**
* Navigates to the given component, effectively making it the new visible
* component. If the given component is actually the previous component in
* the history, {@link #navigateBack()} is performed, otherwise the replaced
* view (previously visible) is pushed onto the view stack.
*
* @param c
* the view to navigate to
*/
public void navigateTo(Component c) {
if (c == null) {
throw new UnsupportedOperationException(
"Some component must always be visible");
} else if (c == getCurrentComponent()) {
/*
* Already navigated to this component.
*/
return;
} else if (getPreviousComponent() == c) {
/*
* Same as navigateBack
*/
navigateBack();
return;
}
if (getNextComponent() != c) {
if (getNextComponent() != null) {
removeComponent(getNextComponent());
getState().setNextComponent(null);
}
addComponent(c);
} else {
getState().setNextComponent(null);
}
if (c instanceof NavigationView) {
NavigationView navigationView = (NavigationView) c;
if (navigationView.getPreviousComponent() == null) {
navigationView.setPreviousComponent(getCurrentComponent());
}
}
if (getPreviousComponent() != null) {
removeComponent(getPreviousComponent());
if (isMaintainBreadcrumb()) {
getViewStack().push(getPreviousComponent());
}
}
getState().setPreviousComponent(getCurrentComponent());
getState().setCurrentComponent(c);
notifyViewOfBecomingVisible();
markAsDirty();
fireEvent(new NavigationEvent(this, Direction.FORWARD));
}
private void notifyViewOfBecomingVisible() {
if (getCurrentComponent() instanceof NavigationView) {
NavigationView v = (NavigationView) getCurrentComponent();
v.onBecomingVisible();
/*
* TODO consider forcing setting the previous component here.
*/
}
}
/**
* Navigates backwards in history by popping the previous component off of
* the view stack and making it visible. The currently visible view is
* replaced and cached for a short while (see {@link #getNextComponent()} in
* case the user wishes to return to the same view.
*/
public void navigateBack() {
if (getPreviousComponent() == null) {
return;
}
if (getNextComponent() != null) {
removeComponent(getNextComponent());
}
// nextComponent is kept for the animation and in case the user
// navigates 'back to the future':
getState().setNextComponent(getCurrentComponent());
getState().setCurrentComponent(getPreviousComponent());
if (isMaintainBreadcrumb()) {
getState().setPreviousComponent(
getViewStack().isEmpty() ? null : getViewStack().pop());
} else {
getState().setPreviousComponent(null);
}
if (getPreviousComponent() != null) {
addComponent(getPreviousComponent());
}
notifyViewOfBecomingVisible();
markAsDirty();
fireEvent(new NavigationEvent(this, Direction.BACK));
}
/**
* Sets the currently visible component in the NavigationManager.
* <p>
* If the current component is already set it is overridden. If the previous
* component or the next component is of type NavigationView, their next and
* previous components will be automatically re-assigned.
*
* @param newCurrentComponent
* the component to set as the currently visible component.
*/
public void setCurrentComponent(Component newCurrentComponent) {
if (getCurrentComponent() != newCurrentComponent) {
if (getCurrentComponent() != null) {
removeComponent(getCurrentComponent());
}
getState().setCurrentComponent(newCurrentComponent);
addComponent(newCurrentComponent);
if (getPreviousComponent() != null
&& getCurrentComponent() instanceof NavigationView) {
NavigationView view = (NavigationView) getCurrentComponent();
view.setPreviousComponent(getPreviousComponent());
}
if (getNextComponent() != null
&& getNextComponent() instanceof NavigationView) {
NavigationView view = (NavigationView) getNextComponent();
view.setPreviousComponent(getCurrentComponent());
}
markAsDirty();
}
}
/**
* @return the component that is currently visible
*/
public Component getCurrentComponent() {
return (Component) getState().getCurrentComponent();
}
/**
* Replaces the topmost component in the history, forgetting the replaced
* component - i.e modifies the history.
*
* @param newPreviousComponent
* the new previous component
*/
public void setPreviousComponent(Component newPreviousComponent) {
if (getPreviousComponent() != newPreviousComponent) {
if (getPreviousComponent() != null) {
removeComponent(getPreviousComponent());
}
getState().setPreviousComponent(newPreviousComponent);
if (getCurrentComponent() instanceof NavigationView) {
NavigationView view = (NavigationView) getCurrentComponent();
view.setPreviousComponent(newPreviousComponent);
}
if (getPreviousComponent() != null) {
addComponent(newPreviousComponent);
}
markAsDirty();
}
}
/**
* @return the previous component, e.g. the top of the view stack, or null
* if n/a
*/
public Component getPreviousComponent() {
return (Component) getState().getPreviousComponent();
}
/**
* If the developer knows the next component where user is going to
* navigate, it can be set with this method. This might allow the component
* to be pre-rendered before the actual navigation (and animation) occurs.
* Having a null as nextComponent shows a placeholder content until the next
* view is rendered.
* <p>
* When navigating backwards, this is used to cache the views in case the
* user decides to return to the same view.
*/
public void setNextComponent(Component nextComponent) {
if (this.getNextComponent() == nextComponent) {
return;
}
if (this.getNextComponent() != null) {
removeComponent(this.getNextComponent());
}
getState().setNextComponent(nextComponent);
if (nextComponent != null) {
addComponent(nextComponent);
}
markAsDirty();
}
/**
* @see #setNextComponent(Component)
* @return the next component, or null if none set
*/
public Component getNextComponent() {
return (Component) getState().getNextComponent();
}
/**
* This operation is not supported
*
* @throws UnsupportedOperationException
*/
@Override
public void replaceComponent(Component oldComponent, Component newComponent) {
throw new UnsupportedOperationException();
}
@Override
public Iterator<Component> getComponentIterator() {
ArrayList<Component> components = getComponents();
return components.iterator();
}
private ArrayList<Component> getComponents() {
ArrayList<Component> components = new ArrayList<Component>(3);
if (getPreviousComponent() != null) {
components.add(getPreviousComponent());
}
if (getCurrentComponent() != null) {
components.add(getCurrentComponent());
}
if (getNextComponent() != null) {
components.add(getNextComponent());
}
return components;
}
/**
* A NavigationEvent is triggered when the user navigates forward or
* backward in the NavigationManager.
*/
public static class NavigationEvent extends com.vaadin.ui.Component.Event {
public enum Direction {
BACK, FORWARD
}
private Direction direction;
/**
* Constructs a NavigationEvent with the given source and direction of
* navigation.
*
* @param source
* the source
* @param direction
* the direction of navigation
*/
public NavigationEvent(Component source, Direction direction) {
super(source);
this.direction = direction;
}
/**
* @return the direction of navigation
*/
public Direction getDirection() {
return direction;
}
}
/**
* A NavigationListener is notified whenever a navigation event occurs.
* NavigationListeners can be used for setting the next view in cases where
* it is known. See {@link #setNextComponent(com.vaadin.ui.Component)}
*/
public interface NavigationListener extends ConnectorEventListener {
Method METHOD = ReflectTools.findMethod(NavigationListener.class,
"navigate", NavigationEvent.class);
/**
* Called when a navigation event is triggered
*
* @param event
* the navigation event.
*/
public void navigate(NavigationEvent event);
}
/**
* Adds a navigation listener that is notified whenever a navigation event
* occurs.
*
* @param listener
* the listener to add.
*/
public void addNavigationListener(NavigationListener listener) {
addListener(NavigationEvent.class, listener, NavigationListener.METHOD);
}
/**
* Removes a navigation listener.
*
* @param listener
* the listener to remove.
*/
public void removeListener(NavigationListener listener) {
removeListener(NavigationEvent.class, listener,
NavigationListener.METHOD);
}
/**
* @return true if NavigationManager should maintain a breadcrumb of visited
* views. The default is true.
* @see #setMaintainBreadcrumb(boolean)
*/
public boolean isMaintainBreadcrumb() {
return maintainBreadcrumb;
}
/**
* Configures whether the NavigationManager maintains a breadcrumb of
* visited views automatically. This is handy when using NavigationViews to
* dive into a deep hierarchy.
*
* @param maintainBreadcrumb
* true if a breadcrumb should be maintained
*/
public void setMaintainBreadcrumb(boolean maintainBreadcrumb) {
this.maintainBreadcrumb = maintainBreadcrumb;
}
@Override
public int getComponentCount() {
return getComponents().size();
}
@Override
public Iterator<Component> iterator() {
return getComponentIterator();
}
}