com.google.livingstories.client.ui.PartialDisclosurePanel.java Source code

Java tutorial

Introduction

Here is the source code for com.google.livingstories.client.ui.PartialDisclosurePanel.java

Source

/**
 * Copyright 2010 Google Inc.
 *
 * 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.google.livingstories.client.ui;

import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
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.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasAnimation;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.livingstories.client.util.Constants;
import com.google.livingstories.client.util.LivingStoryControls;

import java.util.List;

/**
 * A widget that consists of a header and a content panel.  Some items within the
 * content panel are initially hidden, and clicking the header will toggle the display
 * state of these items.  The panel will also animate, expanding and collapsing
 * to accomodate these elements.
 * 
 * This class uses the same style rules as the standard gwt DisclosurePanel.
 * .gwt-DisclosurePanel { the panel's primary style }
 * .gwt-DisclosurePanel-open { dependent style set when panel is open }
 * .gwt-DisclosurePanel-closed { dependent style set when panel is closed }
 * .gwt-DisclosurePanel .header { style for the header }
 * .gwt-DisclosurePanel .content { style for the content }
 * 
 * Quite a bit of this code comes directly from the standard DisclosurePanel itself;
 * however, extending that class didn't seem practical for our use case, so we rewrite
 * some functionality here.
 */
public final class PartialDisclosurePanel extends Composite implements HasAnimation,
        HasOpenHandlers<PartialDisclosurePanel>, HasCloseHandlers<PartialDisclosurePanel>, HasClickHandlers {
    /**
     * Used to wrap widgets in the header to provide click support. Effectively
     * wraps the widget in an <code>anchor</code> to get automatic keyboard
     * access.
     */
    private final class ClickableHeader extends SimplePanel implements HasClickHandlers {

        private ClickableHeader() {
            // Anchor is used to allow keyboard access.
            super(DOM.createAnchor());
            Element elem = getElement();
            DOM.setElementProperty(elem, "href", "javascript:void(0);");
            // Avoids layout problems from having blocks in inlines.
            DOM.setStyleAttribute(elem, "display", "block");
            sinkEvents(Event.ONCLICK);
            setStyleName(STYLENAME_HEADER);
        }

        public HandlerRegistration addClickHandler(ClickHandler handler) {
            return addHandler(handler, ClickEvent.getType());
        }

        @Override
        public void onBrowserEvent(Event event) {
            // no need to call super.
            switch (DOM.eventGetType(event)) {
            case Event.ONCLICK:
                // Prevent link default action.
                DOM.eventPreventDefault(event);
                ClickEvent.fireNativeEvent(event, this);
                setOpen(!isOpen);
            }
        }
    }

    // Stylename constants.
    private static final String STYLENAME_DEFAULT = "gwt-DisclosurePanel";

    private static final String STYLENAME_SUFFIX_OPEN = "open";

    private static final String STYLENAME_SUFFIX_CLOSED = "closed";

    private static final String STYLENAME_HEADER = "header";

    private static final String STYLENAME_CONTENT = "content";

    private final VerticalPanel mainPanel = new VerticalPanel();
    private final ClickableHeader header = new ClickableHeader();
    private final SimplePanel contentWrapper = new SimplePanel();

    private boolean isAnimationEnabled = false;
    private boolean isOpen = false;
    private List<Widget> toggledWidgets;
    private Command onAnimationCompletion;
    private Command oneTimeOnAnimationCompletion;

    public PartialDisclosurePanel(boolean headerOnTop) {
        init(headerOnTop);
    }

    public PartialDisclosurePanel(Widget panelHeader, boolean headerOnTop) {
        this(headerOnTop);
        setHeader(panelHeader);
    }

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

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

    public HandlerRegistration addClickHandler(ClickHandler handler) {
        return header.addClickHandler(handler);
    }

    public void setAnimationCompletionCommand(Command onAnimationCompletion) {
        this.onAnimationCompletion = onAnimationCompletion;
    }

    public void clear() {
        setContent(null, null);
    }

    public Widget getContent() {
        return contentWrapper.getWidget();
    }

    public Widget getHeader() {
        return header.getWidget();
    }

    public HasText getHeaderTextAccessor() {
        Widget widget = header.getWidget();
        return (widget instanceof HasText) ? (HasText) widget : null;
    }

    public boolean isAnimationEnabled() {
        return isAnimationEnabled;
    }

    public boolean isOpen() {
        return isOpen;
    }

    public void setAnimationEnabled(boolean enable) {
        isAnimationEnabled = enable;
    }

    /**
     * Sets the content widget which can be opened and closed by this panel. If
     * there is a preexisting content widget, it will be detached.
     * 
     * @param content the widget to be used as the content panel
     * @param toggledWidgets items that will be shown and hidden by this panel.
     */
    public void setContent(Widget content, List<Widget> toggledWidgets) {
        final Widget currentContent = getContent();

        // Remove existing content widget.
        if (currentContent != null) {
            contentWrapper.setWidget(null);
            this.toggledWidgets = null;
            currentContent.removeStyleName(STYLENAME_CONTENT);
        }

        // Add new content widget if != null.
        if (content != null) {
            contentWrapper.setWidget(content);
            this.toggledWidgets = toggledWidgets;
            content.addStyleName(STYLENAME_CONTENT);
        }
    }

    /**
     * Sets the widget used as the header for the panel.
     * 
     * @param headerWidget the widget to be used as the header
     */
    public void setHeader(Widget headerWidget) {
        header.setWidget(headerWidget);
    }

    /**
     * Changes the visible state of this <code>DisclosurePanel</code>.
     * 
     * @param isOpen <code>true</code> to open the panel, <code>false</code> to
     * close
     */
    public void setOpen(boolean isOpen) {
        if (this.isOpen != isOpen) {
            this.isOpen = isOpen;
            setContentDisplay(isAnimationEnabled);
            fireEvent();
        }
    }

    public void scrollToContainedWidget(final Widget w) {
        Command doScroll = new Command() {
            @Override
            public void execute() {
                Window.scrollTo(0, w.getElement().getAbsoluteTop());
                LivingStoryControls.repositionAnchoredPanel();
            }
        };

        if (isOpen()) {
            doScroll.execute();
        } else {
            setOneTimeAnimationCompletionCommand(doScroll);
            setOpen(true);
        }
    }

    /**
     * <b>Affected Elements:</b>
     * <ul>
     * <li>-header = the clickable header.</li>
     * </ul>
     * 
     * @see UIObject#onEnsureDebugId(String)
     */
    @Override
    protected void onEnsureDebugId(String baseID) {
        super.onEnsureDebugId(baseID);
        header.ensureDebugId(baseID + "-header");
    }

    private void setOneTimeAnimationCompletionCommand(Command oneTimeOnAnimationCompletion) {
        this.oneTimeOnAnimationCompletion = oneTimeOnAnimationCompletion;
    }

    private void fireEvent() {
        if (isOpen) {
            OpenEvent.fire(this, this);
        } else {
            CloseEvent.fire(this, this);
        }
    }

    private void init(boolean headerOnTop) {
        initWidget(mainPanel);
        if (headerOnTop) {
            mainPanel.add(header);
            mainPanel.add(contentWrapper);
        } else {
            mainPanel.add(contentWrapper);
            mainPanel.add(header);
        }
        DOM.setStyleAttribute(contentWrapper.getElement(), "padding", "0px");
        DOM.setStyleAttribute(contentWrapper.getElement(), "overflow", "hidden");
        setStyleName(STYLENAME_DEFAULT);
        addStyleDependentName(STYLENAME_SUFFIX_CLOSED);
    }

    private void setContentDisplay(boolean animate) {
        if (isOpen) {
            removeStyleDependentName(STYLENAME_SUFFIX_CLOSED);
            addStyleDependentName(STYLENAME_SUFFIX_OPEN);
        } else {
            removeStyleDependentName(STYLENAME_SUFFIX_OPEN);
            addStyleDependentName(STYLENAME_SUFFIX_CLOSED);
        }

        if (getContent() != null) {
            int oldHeight = getContent().getElement().getClientHeight();
            DOM.setStyleAttribute(contentWrapper.getElement(), "height", oldHeight + "px");

            for (Widget widget : toggledWidgets) {
                widget.setVisible(!widget.isVisible());
            }

            int newHeight = getContent().getElement().getClientHeight();

            if (animate) {
                ExpandEffect animation = new ExpandEffect(contentWrapper, newHeight);
                animation.run(Constants.ANIMATION_DURATION);
            } else {
                DOM.setStyleAttribute(contentWrapper.getElement(), "height", "auto");
                runCompletionCode();
            }
        }
    }

    private class ExpandEffect extends StyleEffect {
        public ExpandEffect(Widget widget, int newValue) {
            super(widget, "height", newValue);
        }

        @Override
        public void onComplete() {
            DOM.setStyleAttribute(widget.getElement(), "height", "auto");
            runCompletionCode();
        }
    }

    private void runCompletionCode() {
        // for IE friendliness, we always run the completion code via the DeferredCommand
        // mechanism.
        DeferredCommand.addCommand(new Command() {
            @Override
            public void execute() {
                if (onAnimationCompletion != null) {
                    onAnimationCompletion.execute();
                }
                if (oneTimeOnAnimationCompletion != null) {
                    oneTimeOnAnimationCompletion.execute();
                    oneTimeOnAnimationCompletion = null;
                }
            }
        });
    }
}