/* * Copyright 2012 Cedric Hauber * * 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 org.jboss.errai.mvp.client.presenters; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.GwtEvent.Type; import com.google.gwt.event.shared.HasHandlers; import com.google.gwt.user.client.ui.Widget; import com.google.web.bindery.event.shared.EventBus; import com.google.web.bindery.event.shared.HandlerRegistration; import org.jboss.errai.mvp.client.events.ResetPresentersEvent; import org.jboss.errai.mvp.client.views.PopupView; import org.jboss.errai.mvp.client.views.PopupViewCloseHandler; import org.jboss.errai.mvp.client.views.View; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A presenter that does not have to be a singleton. Pages from your * application will usually be singletons and extend the {@link Presenter} class. * <p /> * Choosing between a {@link Presenter} and {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} is a design decision that * requires some thought. For example, a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} is useful when * you need a custom widget with extensive logic. For example, a chat box that can be instantiated * multiple times and needs to communicate with the server would be a good candidate for * a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget}. The drawback of a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} is that it is managed by its * parent presenter, which increases coupling. Therefore, you should use a {@link Presenter} when * the parent is not expected to know its child. Moreover, only {@link Presenter} can be attached * to name tokens in order to support browser history. * <p /> * {@link org.jboss.errai.mvp.client.presenters.PresenterWidget}s and {@link Presenter}s are organized in a hierarchy. * Internally, parent presenters have links to their currently attached children presenters. A * parent {@link Presenter} can contain either {@link Presenter}s or {@link org.jboss.errai.mvp.client.presenters.PresenterWidget}s, * but a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} can only contain {@link org.jboss.errai.mvp.client.presenters.PresenterWidget}s. * <p /> * To reveal a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} you should insert it within a {@link HasSlots slot} of its * containing presenter using one of the following methods: * <ul> * <li>{@link #setInSlot(Object, org.jboss.errai.mvp.client.presenters.PresenterWidget)} * <li>{@link #setInSlot(Object, org.jboss.errai.mvp.client.presenters.PresenterWidget, boolean)} * <li>{@link #addToSlot(Object, org.jboss.errai.mvp.client.presenters.PresenterWidget)} * <li>{@link #addToPopupSlot(org.jboss.errai.mvp.client.presenters.PresenterWidget)} * <li>{@link #addToPopupSlot(org.jboss.errai.mvp.client.presenters.PresenterWidget, boolean)} * </ul> * Revealing a {@link Presenter} is done differently, refer to the class documentation for more details. * <p /> * To hide a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} or a {@link Presenter} you can use {@link #setInSlot} to place * another presenter in the same slot, or you can call one of the following methods: * <ul> * <li>{@link #removeFromSlot(Object, org.jboss.errai.mvp.client.presenters.PresenterWidget)} * <li>{@link #clearSlot(Object)} * <li>{@link PopupView#hide()} if the presenter is a popup or a dialog box. * </ul> * Hide a {@link Presenter} using these methods, but * <p /> * A presenter has a number of lifecycle methods that you can hook on to: * <ul> * <li>{@link #onBind()} * <li>{@link #onReveal()} * <li>{@link #onReset()} * <li>{@link #onHide()} * <li>{@link #onUnbind()} * </ul> * Revealing or hiding a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} triggers an internal chain of events that result in * these lifecycle methods being called. For an example, here is what happens following * a call to {@link #setInSlot(Object, org.jboss.errai.mvp.client.presenters.PresenterWidget)}: * <ul> * <li>If a presenter already occupies this slot it is removed.</li> * <ul><li>If the presenter owning the slot is currently visible then * {@link #onHide()} is called on the removed presenter and, recursively, * on its children (bottom-up: first the children, then the parent)</li> * <li>If the parent is not visible and is a {@link Presenter}, it asks to be * set in one of its parent slot by firing a * {@link org.jboss.errai.mvp.client.events.RevealContentEvent RevealContentEvent}. * For more details, see the documentation for {@link Presenter}.</li> * </ul> * <li>If, at this point, the presenter owning the slot is not visible, then the * chain stops. Otherwise, {@link #onReveal()} is called on the {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} that * was just added.</li> * <li>{@link #onReveal()} is called recursively on that presenter's children * (top-down: first the parent, then the children).</li> * <li>If {@link #setInSlot(Object, org.jboss.errai.mvp.client.presenters.PresenterWidget, boolean)} was called with {@code false} * as the third parameter then the process stops. Otherwise, {@link #onReset()} is * called on all the currently visible presenters (top-down: first the parent, then * the children).</li> * </ul> * * @param <V> The {@link View} type. * * @author Philippe Beaudoin * @author Christian Goudreau * @author Denis Labaye */ public abstract class PresenterWidget<V extends View> extends HandlerContainerImpl implements HasHandlers, HasSlots, HasPopupSlot { private final EventBus eventBus; private final V view; boolean visible; /** * This map makes it possible to keep a list of all the active children in * every slot managed by this {@link org.jboss.errai.mvp.client.presenters.PresenterWidget}. A slot is identified by an * opaque object. A single slot can have many children. */ private final Map<Object, List<PresenterWidget<?>>> activeChildren = new HashMap<Object, List<PresenterWidget<?>>>(); /** * The parent presenter, in order to make sure this widget is only ever in one * parent. */ private PresenterWidget<?> currentParentPresenter; /** * This list tracks all the active children in popup slots managed by this * {@link org.jboss.errai.mvp.client.presenters.PresenterWidget}. A slot is identified by an opaque object. A * single slot can have many children. */ private final List<PresenterWidget<? extends PopupView>> popupChildren = new ArrayList<PresenterWidget<? extends PopupView>>(); /** * Creates a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} that is not necessarily using automatic * binding. Automatic binding will only work when instantiating this object using * Guice/GIN dependency injection. See * {@link HandlerContainerImpl#HandlerContainerImpl(boolean)} for * more details on automatic binding. * * @param autoBind {@code true} to request automatic binding, {@code false} otherwise. * @param eventBus The {@link com.google.web.bindery.event.shared.EventBus}. * @param view The {@link View}. */ public PresenterWidget(boolean autoBind, EventBus eventBus, V view) { super(autoBind); this.eventBus = eventBus; this.view = view; } /** * Creates a {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} that uses automatic binding. This will * only work when instantiating this object using Guice/GIN dependency injection. * See {@link HandlerContainerImpl#HandlerContainerImpl()} for more details on * automatic binding. * * @param eventBus The {@link com.google.web.bindery.event.shared.EventBus}. * @param view The {@link View}. */ public PresenterWidget(EventBus eventBus, V view) { this(true, eventBus, view); } @Override public final void addToPopupSlot( final PresenterWidget<? extends PopupView> child) { addToPopupSlot(child, true); } @Override public final void addToPopupSlot( final PresenterWidget<? extends PopupView> child, boolean center) { if (child == null) { return; } child.reparent(this); // Do nothing if the content is already added for (PresenterWidget<?> popupPresenter : popupChildren) { if (popupPresenter == child) { return; } } final PopupView popupView = child.getView(); popupChildren.add(child); // Center if desired if (center) { popupView.center(); } // Display the popup content if (isVisible()) { popupView.show(); // This presenter is visible, its time to call onReveal // on the newly added child (and recursively on this child children) monitorCloseEvent(child); if (!child.isVisible()) { child.internalReveal(); } } } @Override public final void addToSlot(Object slot, PresenterWidget<?> content) { if (content == null) { return; } content.reparent(this); List<PresenterWidget<?>> slotChildren = activeChildren.get(slot); if (slotChildren != null) { slotChildren.add(content); } else { slotChildren = new ArrayList<PresenterWidget<?>>(1); slotChildren.add(content); activeChildren.put(slot, slotChildren); } getView().addToSlot(slot, content.getWidget()); if (isVisible()) { // This presenter is visible, its time to call onReveal // on the newly added child (and recursively on this child children) content.internalReveal(); } } @Override public final void clearSlot(Object slot) { List<PresenterWidget<?>> slotChildren = activeChildren.get(slot); if (slotChildren != null) { // This presenter is visible, its time to call onHide // on the children to be removed (and recursively on their children) if (isVisible()) { for (PresenterWidget<?> activeChild : slotChildren) { activeChild.internalHide(); } } slotChildren.clear(); } getView().setInSlot(slot, null); } @Override public void fireEvent(GwtEvent<?> event) { getEventBus().fireEventFromSource(event, this); } /** * Returns the {@link View} for the current presenter. * * @return The view. */ public V getView() { return view; } /** * Makes it possible to access the {@link com.google.gwt.user.client.ui.Widget} object associated with that * presenter. * * @return The Widget associated with that presenter. */ public Widget getWidget() { return (getView() == null) ? null : getView().asWidget(); } /** * Verifies if the presenter is currently visible on the screen. A presenter * should be visible if it successfully revealed itself and was not hidden * later. * * @return {@code true} if the presenter is visible, {@code false} otherwise. */ public boolean isVisible() { return visible; } @Override public final void removeFromSlot(Object slot, PresenterWidget<?> content) { if (content == null) { return; } content.reparent(null); List<PresenterWidget<?>> slotChildren = activeChildren.get(slot); if (slotChildren != null) { // This presenter is visible, its time to call onHide // on the child to be removed (and recursively on itschildren) if (isVisible()) { content.internalHide(); } slotChildren.remove(content); } getView().removeFromSlot(slot, content.getWidget()); } // TODO This should be final but needs to be overriden in {@link // TabContainerPresenter} // We should be able to do this once we switch to an event-based mechanism for // highlighting tabs @Override public void setInSlot(Object slot, PresenterWidget<?> content) { setInSlot(slot, content, true); } @Override public final void setInSlot(Object slot, PresenterWidget<?> content, boolean performReset) { if (content == null) { // Assumes the user wants to clear the slot content. clearSlot(slot); return; } content.reparent(this); List<PresenterWidget<?>> slotChildren = activeChildren.get(slot); if (slotChildren != null) { if (slotChildren.size() == 1 && slotChildren.get(0) == content) { // The slot contains the right content, nothing to do return; } if (isVisible()) { // We are visible, make sure the content that we're removing // is being notified as hidden for (PresenterWidget<?> activeChild : slotChildren) { activeChild.internalHide(); } } slotChildren.clear(); slotChildren.add(content); } else { slotChildren = new ArrayList<PresenterWidget<?>>(1); slotChildren.add(content); activeChildren.put(slot, slotChildren); } // Set the content in the view getView().setInSlot(slot, content.getWidget()); if (isVisible()) { // This presenter is visible, its time to call onReveal // on the newly added child (and recursively on this child children) if (!content.isVisible()) { content.internalReveal(); } if (performReset) { // And to reset everything if needed ResetPresentersEvent.fire(this); } } } /** * Registers an event handler towards the {@link com.google.web.bindery.event.shared.EventBus} and * registers it to be automatically removed when {@link #unbind()} * is called. This is usually the desired behavior, but if you * want to unregister handlers manually use {@link #addHandler} * instead. * * @see #addHandler(com.google.gwt.event.shared.GwtEvent.Type, com.google.gwt.event.shared.EventHandler) * * @param <H> The handler type. * @param type See {@link com.google.gwt.event.shared.GwtEvent.Type}. * @param handler The handler to register. */ protected final <H extends EventHandler> void addRegisteredHandler( Type<H> type, H handler) { registerHandler(addHandler(type, handler)); } /** * Registers an event handler towards the {@link com.google.web.bindery.event.shared.EventBus}. * Use this only in the rare situations where you want to manually * control when the handler is unregistered, otherwise call * {@link #addRegisteredHandler(com.google.gwt.event.shared.GwtEvent.Type, com.google.gwt.event.shared.EventHandler)}. * * @param <H> The handler type. * @param type See {@link com.google.gwt.event.shared.GwtEvent.Type}. * @param handler The handler to register. * @return The {@link com.google.web.bindery.event.shared.HandlerRegistration} you should use to unregister the handler. */ protected final <H extends EventHandler> HandlerRegistration addHandler( Type<H> type, H handler) { return getEventBus().addHandler(type, handler); } /** * Access the {@link com.google.web.bindery.event.shared.EventBus} object associated with that presenter. * You should not usually use this method to interact with the event bus. * Instead call {@link #fireEvent}, {@link #addRegisteredHandler} or * {@link #addHandler}. * * @return The EventBus associated with that presenter. */ protected final EventBus getEventBus() { return eventBus; } /** * Lifecycle method called whenever this presenter is about to be * hidden. * <p /> * <b>Important:</b> Make sure you call your superclass {@link #onHide()} if * you override. Also, do not call directly, see {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} * for more details on lifecycle methods. * <p /> * You should override this method to dispose of any object * created directly or indirectly during the call to {@link #onReveal()}. * <p /> * This method will not be invoked a multiple times without {@link #onReveal()} * being called. * <p /> * In a presenter hierarchy, this method is called bottom-up: first on the * child presenters, then on the parent. */ protected void onHide() { } /** * Lifecycle method called on all visible presenters whenever a * presenter is revealed anywhere in the presenter hierarchy. * <p /> * <b>Important:</b> Make sure you call your superclass {@link #onReset()} if * you override. Also, do not call directly, fire a {@link ResetPresentersEvent} * to perform a reset manually. See {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} for more details on * lifecycle methods. * <p /> * This is one of the most frequently used lifecycle method. This is usually a good * place to refresh any information displayed by your presenter. * <p /> * Note that {@link #onReset()} is not called only when using * {@link #addToSlot(Object, org.jboss.errai.mvp.client.presenters.PresenterWidget)}, {@link #addToPopupSlot(org.jboss.errai.mvp.client.presenters.PresenterWidget)} * or #setInSlot(Object, PresenterWidget, boolean)} with {@code false} as the third * parameter. * <p /> * In a presenter hierarchy, this method is called top-down: first on the * parent presenters, then on the children. */ protected void onReset() { } /** * Lifecycle method called whenever this presenter is about to be * revealed. * <p /> * <b>Important:</b> Make sure you call your superclass {@link #onReveal()} if * you override. Also, do not call directly, see {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} * for more details on lifecycle methods. * <p /> * You should override this method to perform any action or initialisation * that needs to be done when the presenter is revealed. Any initialisation * you perform here should be taken down in {@link #onHide()}. * <p /> * Information that needs to be updated whenever the user navigates should * be refreshed in {@link #onReset()}. * <p /> * In a presenter hierarchy, this method is called top-down: first on the * parent presenters, then on the children. */ protected void onReveal() { } /** * Internal method called to hide a presenter. * See {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} for ways to hide a presenter. */ void internalHide() { assert isVisible() : "Hide() called on a hidden presenter!"; for (List<PresenterWidget<?>> slotChildren : activeChildren.values()) { for (PresenterWidget<?> activeChild : slotChildren) { activeChild.internalHide(); } } for (PresenterWidget<? extends PopupView> popupPresenter : popupChildren) { popupPresenter.getView().setCloseHandler(null); popupPresenter.internalHide(); popupPresenter.getView().hide(); } visible = false; onHide(); } /** * Internal method called to reveal a presenter. * See {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} and {@link Presenter} for ways to reveal a * presenter. */ void internalReveal() { assert !isVisible() : "notifyReveal() called on a visible presenter!"; onReveal(); visible = true; for (List<PresenterWidget<?>> slotChildren : activeChildren.values()) { for (PresenterWidget<?> activeChild : slotChildren) { activeChild.internalReveal(); } } for (PresenterWidget<? extends PopupView> popupPresenter : popupChildren) { // This presenter is visible, its time to call onReveal // on the newly added child (and recursively on this child children) popupPresenter.getView().show(); monitorCloseEvent(popupPresenter); popupPresenter.internalReveal(); } } /** * Detaches this presenter from its current parent and attaches it * to a new parent. * * @param newParent The new parent {@link org.jboss.errai.mvp.client.presenters.PresenterWidget}. */ void reparent(PresenterWidget<?> newParent) { if (currentParentPresenter != null && currentParentPresenter != newParent) { currentParentPresenter.detach(this); } currentParentPresenter = newParent; } /** * Internal method called to reset a presenter. Instead of using that method, * fire a {@link ResetPresentersEvent} to perform a reset manually. */ void internalReset() { onReset(); for (List<PresenterWidget<?>> slotChildren : activeChildren.values()) { for (PresenterWidget<?> activeChild : slotChildren) { activeChild.internalReset(); } } for (PresenterWidget<?> popupPresenter : popupChildren) { popupPresenter.internalReset(); } } /** * Called by a child {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} when it wants to detach itself * from this parent. * * @param childPresenter The {@link org.jboss.errai.mvp.client.presenters.PresenterWidget}. It should be a child of this presenter. */ private void detach(PresenterWidget<?> childPresenter) { for (List<PresenterWidget<?>> slotChildren : activeChildren.values()) { slotChildren.remove(childPresenter); } popupChildren.remove(childPresenter); } /** * Monitors the specified popup presenter so that we know when it * is closing. This allows us to make sure it doesn't receive * future messages. * * @param popupPresenter The {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} to monitor. */ private void monitorCloseEvent( final PresenterWidget<? extends PopupView> popupPresenter) { PopupView popupView = popupPresenter.getView(); popupView.setCloseHandler(new PopupViewCloseHandler() { @Override public void onClose() { if (isVisible()) { popupPresenter.internalHide(); } removePopupChildren(popupPresenter); } }); } /** * Go through the popup children and remove the specified one. * * @param content The {@link org.jboss.errai.mvp.client.presenters.PresenterWidget} added as a popup which we want to remove. */ private void removePopupChildren(PresenterWidget<? extends PopupView> content) { int i; for (i = 0; i < popupChildren.size(); ++i) { PresenterWidget<? extends PopupView> popupPresenter = popupChildren .get(i); if (popupPresenter == content) { (popupPresenter.getView()).setCloseHandler(null); break; } } if (i < popupChildren.size()) { popupChildren.remove(i); } } }