org.cruxframework.crux.smartfaces.client.dialog.PopupPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.cruxframework.crux.smartfaces.client.dialog.PopupPanel.java

Source

/*
 * Copyright 2014 cruxframework.org.
 * 
 * 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.cruxframework.crux.smartfaces.client.dialog;

import java.util.ArrayList;
import java.util.List;

import org.cruxframework.crux.core.client.collection.FastList;
import org.cruxframework.crux.core.client.css.animation.Animation;
import org.cruxframework.crux.smartfaces.client.dialog.animation.DialogAnimation;
import org.cruxframework.crux.smartfaces.client.dialog.animation.HasDialogAnimation;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.dom.client.BodyElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.HasCloseHandlers;
import com.google.gwt.event.logical.shared.HasOpenHandlers;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.UIObject;

/**
 * A panel that can "pop up" over other widgets. It overlays the browser's
 * client area (and any previously-created popups).
 * 
 * <p>
 * A PopupPanel should not generally be added to other panels; rather, it should
 * be shown and hidden using the {@link #show()} and {@link #hide()} methods.
 * </p>
 * 
 * @author Thiago da Rosa de Bustamante
 */
public class PopupPanel extends SimplePanel implements HasDialogAnimation, HasCloseHandlers<PopupPanel>,
        HasOpenHandlers<PopupPanel>, NativePreviewHandler {
    public static final String DEFAULT_GLASS_STYLE_NAME = "faces-overlay";

    private HandlerRegistration nativePreviewHandlerRegistration;
    private HandlerRegistration historyHandlerRegistration;
    private boolean autoHideOnHistoryEvents;
    private boolean showing;
    private boolean animationEnabled;
    private boolean modal;
    private boolean autoHide;
    private FastList<Element> autoHidePartners;
    private Element glass;
    private String glassStyleName = DEFAULT_GLASS_STYLE_NAME;
    private boolean glassShowing;
    private boolean centered;
    private Element containerElement;
    private DialogAnimation animation;
    private boolean animating;
    private int left = -1;
    private int top = -1;

    private static List<CloseHandler<PopupPanel>> defaultCloseHandlers = new ArrayList<CloseHandler<PopupPanel>>();
    private static List<OpenHandler<PopupPanel>> defaultOpenHandlers = new ArrayList<OpenHandler<PopupPanel>>();

    /**
     * Creates an empty popup panel. A child widget must be added to it before
     * it is shown.
     */
    public PopupPanel() {
        this(false);
    }

    /**
     * Creates an empty popup panel, specifying its "auto-hide" property.
     * 
     * @param autoHide
     *            <code>true</code> if the popup should be automatically hidden
     *            when the user clicks outside of it or the history token
     *            changes.
     */
    public PopupPanel(boolean autoHide) {
        this(autoHide, false);
    }

    /**
     * Creates an empty popup panel, specifying its "auto-hide" and "modal"
     * properties.
     * 
     * @param autoHide - <code>true</code> if the popup should be automatically hidden
     *            when the user clicks outside of it or the history token
     *            changes.
     * @param modal -  <code>true</code> if keyboard or mouse events that do not
     *            target the PopupPanel or its children should be ignored
     */
    public PopupPanel(boolean autoHide, boolean modal) {
        this.autoHide = autoHide;
        this.autoHideOnHistoryEvents = autoHide;
        this.modal = modal;
        if (modal) {
            glass = Document.get().createDivElement();
            glass.setClassName(glassStyleName);
        }

        addCloseHandler(new CloseHandler<PopupPanel>() {
            @Override
            public void onClose(CloseEvent<PopupPanel> event) {
                if (defaultCloseHandlers != null) {
                    for (CloseHandler<PopupPanel> closeHandler : defaultCloseHandlers) {
                        closeHandler.onClose(event);
                    }
                }
            }
        });

        addOpenHandler(new OpenHandler<PopupPanel>() {
            @Override
            public void onOpen(OpenEvent<PopupPanel> event) {
                if (defaultOpenHandlers != null) {
                    for (OpenHandler<PopupPanel> openHandler : defaultOpenHandlers) {
                        openHandler.onOpen(event);
                    }
                }
            }
        });

        containerElement = Document.get().createDivElement().cast();
        super.getContainerElement().appendChild(containerElement);
        getElement().getStyle().setPosition(Position.ABSOLUTE);
        setPosition(0, 0);
        setStyleName(getContainerElement(), "faces-popup-content");
    }

    /**
     * Mouse events that occur within an autoHide partner will not hide a panel
     * set to autoHide.
     * 
     * @param partner
     *            the auto hide partner to add
     */
    public void addAutoHidePartner(Element partner) {
        assert partner != null : "partner cannot be null";
        if (autoHidePartners == null) {
            autoHidePartners = new FastList<Element>();
        }
        autoHidePartners.add(partner);
    }

    /**
     * Remove an autoHide partner.
     * 
     * @param partner
     *            the auto hide partner to remove
     */
    public void removeAutoHidePartner(Element partner) {
        assert partner != null : "partner cannot be null";
        if (autoHidePartners != null) {
            autoHidePartners.remove(partner);
        }
    }

    /**
     * Determines whether or not this popup is showing.
     * 
     * @return <code>true</code> if the popup is showing
     * @see #show()
     * @see #hide()
     */
    public boolean isShowing() {
        return showing;
    }

    /**
     * Shows the popup and attach it to the page. It must have a child widget
     * before this method is called.
     */
    public void show() {
        doShow(isAnimationEnabled());
    }

    /**
     * Normally, the popup is positioned directly below the relative target,
     * with its left edge aligned with the left edge of the target. Depending on
     * the width and height of the popup and the distance from the target to the
     * bottom and right edges of the window, the popup may be displayed directly
     * above the target, and/or its right edge may be aligned with the right
     * edge of the target.
     * 
     * @param target
     *            the target to show the popup below
     */
    public final void showRelativeTo(final UIObject target) {
        setVisible(false);
        doShow(false);
        setPosition(getLeftRelativeObject(target), getTopRelativeObject(target));
        setVisible(true);
        if (isAnimationEnabled()) {
            runEntranceAnimation(null);
        }
    }

    /**
     * Centers the popup in the browser window and shows it. If the popup was
     * already showing, then it is centered.
     */
    public void center() {
        if (!centered) {
            if (animating) {
                fixPositionToCenter();
                Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() {
                    @Override
                    public boolean execute() {
                        if (animating) {
                            return true;
                        }
                        centralizeMe();
                        return false;
                    }
                }, 10);
            } else {
                centralizeMe();
                if (!isShowing()) {
                    show();
                }
            }
        }
    }

    /**
     * Hides the popup and detaches it from the page. This has no effect if it
     * is not currently showing.
     */
    public void hide() {
        hide(false);
    }

    /**
     * Sets the style name to be used on the glass element. 
     * 
     * @param glassStyleName
     *            the glass element's style name
     */
    public void setGlassStyleName(String glassStyleName) {
        this.glassStyleName = glassStyleName;
        if (glass != null) {
            glass.setClassName(glassStyleName);
        }
    }

    /**
     * Gets the style name to be used on the glass element. 
     * 
     * @return the glass element's style name
     */
    public String getGlassStyleName() {
        return glassStyleName;
    }

    @Override
    public HandlerRegistration addCloseHandler(CloseHandler<PopupPanel> handler) {
        return addHandler(handler, CloseEvent.getType());
    }

    @Override
    public HandlerRegistration addOpenHandler(OpenHandler<PopupPanel> handler) {
        return addHandler(handler, OpenEvent.getType());
    }

    /**
     * Defines the animation used to animate popup entrances and exits
     * @param animation
     */
    public void setAnimation(DialogAnimation animation) {
        this.animation = animation;
        setAnimationEnabled(animation != null);
    }

    @Override
    public boolean isAnimationEnabled() {
        return animationEnabled;
    }

    @Override
    public void setAnimationEnabled(boolean enable) {
        animationEnabled = enable;
    }

    /**
     * Enable or disable the autoHide feature. When enabled, the popup will be
     * automatically hidden when the user clicks outside of it.
     * 
     * @param autoHide
     *            true to enable autoHide, false to disable
     */
    public void setAutoHideEnabled(boolean autoHide) {
        this.autoHide = autoHide;
    }

    /**
     * Returns <code>true</code> if the popup should be automatically hidden
     * when the user clicks outside of it.
     * 
     * @return true if autoHide is enabled, false if disabled
     */
    public boolean isAutoHideEnabled() {
        return autoHide;
    }

    /**
     * Returns <code>true</code> if the popup should be automatically hidden
     * when the history token changes, such as when the user presses the
     * browser's back button.
     * 
     * @return true if enabled, false if disabled
     */
    public boolean isAutoHideOnHistoryEventsEnabled() {
        return autoHideOnHistoryEvents;
    }

    /**
     * Returns <code>true</code> if the popup opens a modal window
     * @return true if modal
     */
    public boolean isModal() {
        return modal;
    }

    /**
     * Enable or disable autoHide on history change events. When enabled, the
     * popup will be automatically hidden when the history token changes, such
     * as when the user presses the browser's back button. Disabled by default.
     * 
     * @param enabled
     *            true to enable, false to disable
     */
    public void setAutoHideOnHistoryEventsEnabled(boolean enabled) {
        this.autoHideOnHistoryEvents = enabled;
    }

    @Override
    @SuppressWarnings("deprecation")
    protected com.google.gwt.user.client.Element getContainerElement() {
        return containerElement.cast();
    }

    /**
     * Sets the popup's position relative to the browser's client area. The
     * popup's position may be set before calling {@link #show()}.
     * 
     * @param left
     *            the left position, in pixels
     * @param top
     *            the top position, in pixels
     */
    public void setPosition(int left, int top) {
        if (centered) {
            uncentralizeMe();
        }

        // Account for the difference between absolute position and the
        // body's positioning context.
        Document document = Document.get();
        left -= document.getBodyOffsetLeft();
        top -= document.getBodyOffsetTop();

        this.left = left;
        this.top = top;
        setPopupPositionStyle(left, top);
    }

    /**
     * Sets whether this object is visible. This method just sets the
     * <code>visibility</code> style attribute. You need to call {@link #show()}
     * to actually attached/detach the {@link PopupPanel} to the page.
     * 
     * @param visible
     *            <code>true</code> to show the object, <code>false</code> to
     *            hide it
     * @see #show()
     * @see #hide()
     */
    @Override
    public void setVisible(boolean visible) {
        // We use visibility here instead of UIObject's default of display
        // Because the panel is absolutely positioned, this will not create
        // "holes" in displayed contents and it allows normal layout passes
        // to occur so the size of the PopupPanel can be reliably determined.
        getElement().getStyle().setVisibility(visible ? Visibility.VISIBLE : Visibility.HIDDEN);

        if (glass != null) {
            glass.getStyle().setVisibility(visible ? Visibility.VISIBLE : Visibility.HIDDEN);
        }
    }

    /**
     * Determines whether or not this popup is visible. Note that this just
     * checks the <code>visibility</code> style attribute, which is set in the
     * {@link #setVisible(boolean)} method. If you want to know if the popup is
     * attached to the page, use {@link #isShowing()} instead.
     * 
     * @return <code>true</code> if the object is visible
     * @see #setVisible(boolean)
     */
    @Override
    public boolean isVisible() {
        return !getElement().getStyle().getVisibility().equals(Visibility.HIDDEN.getCssName());
    }

    @Override
    public void onPreviewNativeEvent(NativePreviewEvent event) {
        if (event.isCanceled()) {
            return;
        }

        // If the event targets the popup or the partner, consume it
        Event nativeEvent = Event.as(event.getNativeEvent());
        boolean eventTargetsPopupOrPartner = eventTargetsPopup(nativeEvent) || eventTargetsPartner(nativeEvent);
        if (eventTargetsPopupOrPartner) {
            event.consume();
        }

        // Switch on the event type
        int type = nativeEvent.getTypeInt();
        switch (type) {
        case Event.ONMOUSEDOWN:
        case Event.ONTOUCHSTART:
            // Don't eat events if event capture is enabled, as this can
            // interfere with dialog dragging, for example.
            if (DOM.getCaptureElement() != null) {
                event.consume();
                return;
            }

            if (!eventTargetsPopupOrPartner && autoHide) {
                hide(true);
                return;
            }
            break;
        case Event.ONMOUSEUP:
        case Event.ONMOUSEMOVE:
        case Event.ONCLICK:
        case Event.ONDBLCLICK:
        case Event.ONTOUCHEND: {
            // Don't eat events if event capture is enabled, as this can
            // interfere with dialog dragging, for example.
            if (DOM.getCaptureElement() != null) {
                event.consume();
                return;
            }
            break;
        }
        }
    }

    @Override
    protected void onUnload() {
        super.onUnload();

        // Just to be sure, we perform cleanup when the popup is unloaded (i.e.
        // removed from the DOM). This is normally taken care of in hide(), but
        // it
        // can be missed if someone removes the popup directly from the
        // RootPanel.
        if (isShowing()) {
            setState(false, true, false, null);
        }
    }

    /**
     * Hides the popup and detaches it from the page. This has no effect if it
     * is not currently showing.
     * 
     * @param autoClosed
     *            the value that will be passed to
     *            {@link CloseHandler#onClose(CloseEvent)} when the popup is
     *            closed
     */
    protected void hide(final boolean autoClosed) {
        doHide(true, autoClosed, isAnimationEnabled());
    }

    private void runEntranceAnimation(final StateChangeCallback callback) {
        animating = true;
        getDialogAnimation().animateEntrance(this, new Animation.Callback() {
            @Override
            public void onAnimationCompleted() {
                animating = false;
                if (callback != null) {
                    callback.onStateChange();
                }
            }
        });
    }

    private DialogAnimation getDialogAnimation() {
        if (animation == null) {
            animation = DialogAnimation.bounce;
        }
        return animation;
    }

    private void doHide(boolean fireEvent, final boolean autoClosed, boolean animated) {
        if (!isShowing()) {
            return;
        }
        if (animated && centered) {
            fixPositionToCenter();
        }
        if (fireEvent) {
            setState(false, false, animated, new StateChangeCallback() {
                @Override
                public void onStateChange() {
                    CloseEvent.fire(PopupPanel.this, PopupPanel.this, autoClosed);
                }
            });
        } else {
            setState(false, false, animated, null);
        }
    }

    private void fixPositionToCenter() {
        int left = getPopupLeftToCenter();
        int top = getPopupTopToCenter();
        setPosition(left, top);
    }

    /**
     * Gets the popup's left position relative to the browser's center area.
     * 
     * @return the popup's left position
     */
    private int getPopupLeftToCenter() {
        int windowLeft = Window.getScrollLeft();
        int windowWidth = Window.getClientWidth();
        int centerLeft = (windowWidth / 2) + windowLeft;

        int offsetWidth = getOffsetWidth();
        return centerLeft - (offsetWidth / 2);
    }

    /**
     * Gets the popup's top position relative to the browser's center area.
     * 
     * @return the popup's top position
     */
    private int getPopupTopToCenter() {
        int windowTop = Window.getScrollTop();
        int windowHeight = Window.getClientHeight();
        int centerTop = (windowHeight / 2) + windowTop;

        int offsetHeight = getOffsetHeight();
        return centerTop - (offsetHeight / 2);
    }

    private void doShow(final boolean animated) {
        if (isShowing()) {
            return;
        } else if (isAttached()) {
            // The popup is attached directly to another panel, so we need to
            // remove
            // it from its parent before showing it. This is a weird use case,
            // but
            // since PopupPanel is a Widget, its legal.
            this.removeFromParent();
        }
        if (centered && animated) {
            setVisible(false);
            setState(true, false, false, new StateChangeCallback() {
                @Override
                public void onStateChange() {
                    OpenEvent.fire(PopupPanel.this, PopupPanel.this);
                }
            });
            fixPositionToCenter();
            setVisible(true);
            runEntranceAnimation(new StateChangeCallback() {
                @Override
                public void onStateChange() {
                    centralizeMe();
                }
            });
        } else {
            setState(true, false, animated, new StateChangeCallback() {
                @Override
                public void onStateChange() {
                    OpenEvent.fire(PopupPanel.this, PopupPanel.this);
                }
            });
        }
    }

    private void centralizeMe() {
        Style style = getElement().getStyle();
        style.setLeft(50, Unit.PCT);
        style.setTop(50, Unit.PCT);
        style.setProperty("webkitTransform", "translateY(-50%) translateX(-50%)");
        style.setProperty("transform", "translateY(-50%) translateX(-50%)");
        left = -1;
        top = -1;
        centered = true;
    }

    private void uncentralizeMe() {
        Style style = getElement().getStyle();
        style.clearProperty("webkitTransform");
        style.clearProperty("transform");
        centered = false;
    }

    private void setPopupPositionStyle(int left, int top) {
        Style style = getElement().getStyle();
        style.setPropertyPx("left", left);
        style.setPropertyPx("top", top);
    }

    private void setState(boolean showing, boolean unloading, boolean animated,
            final StateChangeCallback callback) {
        this.showing = showing;
        updateHandlers();

        maybeShowGlass();
        if (isShowing()) {
            if (animated) {
                animating = true;
                getDialogAnimation().animateEntrance(this, new Animation.Callback() {
                    @Override
                    public void onAnimationCompleted() {
                        animating = false;
                        if (callback != null) {
                            callback.onStateChange();
                        }
                    }
                });
            }
            RootPanel.get().add(this);
            if (!animated && callback != null) {
                callback.onStateChange();
            }
        } else {
            if (!unloading) {
                if (animated) {
                    animating = true;
                    getDialogAnimation().animateExit(this, new Animation.Callback() {
                        @Override
                        public void onAnimationCompleted() {
                            animating = false;
                            removePopupFromDOM();
                            if (callback != null) {
                                callback.onStateChange();
                            }
                        }

                    });
                } else {
                    removePopupFromDOM();
                    if (callback != null) {
                        callback.onStateChange();
                    }
                }
            }
        }
    }

    private void removePopupFromDOM() {
        if (centered) {
            fixPositionToCenter();
        }
        RootPanel.get().remove(PopupPanel.this);
        setPopupPositionStyle(left, top);
    }

    /**
     * Show or hide the glass.
     */
    private void maybeShowGlass() {
        BodyElement body = Document.get().getBody();
        if (isShowing()) {
            if (modal) {
                body.appendChild(glass);
                body.addClassName("unselectable");
                glassShowing = true;
            }
        } else if (glassShowing) {
            body.removeChild(glass);
            body.removeClassName("unselectable");
            glassShowing = false;
        }
    }

    /**
     * Does the event target one of the partner elements?
     * 
     * @param event
     *            the native event
     * @return true if the event targets a partner
     */
    private boolean eventTargetsPartner(NativeEvent event) {
        if (autoHidePartners == null) {
            return false;
        }

        EventTarget target = event.getEventTarget();
        if (Element.is(target)) {
            for (int i = 0; i < autoHidePartners.size(); i++) {
                Element elem = autoHidePartners.get(i);
                if (elem.isOrHasChild(Element.as(target))) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Does the event target this popup?
     * 
     * @param event
     *            the native event
     * @return true if the event targets the popup
     */
    private boolean eventTargetsPopup(NativeEvent event) {
        EventTarget target = event.getEventTarget();
        if (Element.is(target)) {
            return getElement().isOrHasChild(Element.as(target));
        }
        return false;
    }

    /**
     * Register or unregister the handlers used by {@link PopupPanel}.
     */
    private void updateHandlers() {
        // Remove any existing handlers.
        if (nativePreviewHandlerRegistration != null) {
            nativePreviewHandlerRegistration.removeHandler();
            nativePreviewHandlerRegistration = null;
        }
        if (historyHandlerRegistration != null) {
            historyHandlerRegistration.removeHandler();
            historyHandlerRegistration = null;
        }

        // Create handlers if showing.
        if (isShowing()) {
            if (!modal) {
                nativePreviewHandlerRegistration = Event.addNativePreviewHandler(this);
            }
            historyHandlerRegistration = History.addValueChangeHandler(new ValueChangeHandler<String>() {
                public void onValueChange(ValueChangeEvent<String> event) {
                    if (autoHideOnHistoryEvents) {
                        hide();
                    }
                }
            });
        }
    }

    private int getLeftRelativeObject(final UIObject relativeObject) {
        int offsetWidth = getOffsetWidth();
        int relativeElemOffsetWidth = relativeObject.getOffsetWidth();
        int offsetWidthDiff = offsetWidth - relativeElemOffsetWidth;
        int left;

        if (LocaleInfo.getCurrentLocale().isRTL()) { // RTL case

            int relativeElemAbsoluteLeft = relativeObject.getAbsoluteLeft();
            left = relativeElemAbsoluteLeft - offsetWidthDiff;
            if (offsetWidthDiff > 0) {
                int windowRight = Window.getClientWidth() + Window.getScrollLeft();
                int windowLeft = Window.getScrollLeft();

                int relativeElemLeftValForRightEdge = relativeElemAbsoluteLeft + relativeElemOffsetWidth;
                int distanceToWindowRight = windowRight - relativeElemLeftValForRightEdge;
                int distanceFromWindowLeft = relativeElemLeftValForRightEdge - windowLeft;
                if (distanceFromWindowLeft < offsetWidth && distanceToWindowRight >= offsetWidthDiff) {
                    left = relativeElemAbsoluteLeft;
                }
            }
        } else { // LTR case

            left = relativeObject.getAbsoluteLeft();
            if (offsetWidthDiff > 0) {
                int windowRight = Window.getClientWidth() + Window.getScrollLeft();
                int windowLeft = Window.getScrollLeft();
                int distanceToWindowRight = windowRight - left;
                int distanceFromWindowLeft = left - windowLeft;
                if (distanceToWindowRight < offsetWidth && distanceFromWindowLeft >= offsetWidthDiff) {
                    left -= offsetWidthDiff;
                }
            }
        }
        return left;
    }

    private int getTopRelativeObject(final UIObject relativeObject) {
        int offsetHeight = getOffsetHeight();
        int top = relativeObject.getAbsoluteTop();

        int windowTop = Window.getScrollTop();
        int windowBottom = Window.getScrollTop() + Window.getClientHeight();

        int distanceFromWindowTop = top - windowTop;
        int distanceToWindowBottom = windowBottom - (top + relativeObject.getOffsetHeight());

        if (distanceToWindowBottom < offsetHeight && distanceFromWindowTop >= offsetHeight) {
            top -= offsetHeight;
        } else {
            top += relativeObject.getOffsetHeight();
        }
        return top;
    }

    private static interface StateChangeCallback {
        void onStateChange();
    }

    /**
     * Add a default open handler that will be appended to each created object
     * @param defaultOpenHandler
     */
    public static void addDefaultOpenHandler(OpenHandler<PopupPanel> defaultOpenHandler) {
        if (defaultOpenHandlers == null) {
            defaultOpenHandlers = new ArrayList<OpenHandler<PopupPanel>>();
        }
        defaultOpenHandlers.add(defaultOpenHandler);
    }

    /**
     * Add a default close handler that will be appended to each created object
     * @param defaultCloseHandler
     */
    public static void addDefaultCloseHandler(CloseHandler<PopupPanel> defaultCloseHandler) {
        if (defaultCloseHandlers == null) {
            defaultCloseHandlers = new ArrayList<CloseHandler<PopupPanel>>();
        }
        defaultCloseHandlers.add(defaultCloseHandler);
    }
}