/* * Copyright 2008 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.gwt.user.client.ui; import com.google.gwt.animation.client.Animation; import com.google.gwt.core.client.GWT; 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.resources.client.ClientBundle; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.resources.client.ImageResource.ImageOptions; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; import java.util.Iterator; /** * A widget that consists of a header and a content panel that discloses the * content when a user clicks on the header. * * <h3>CSS Style Rules</h3> * <dl class="css"> * <dt>.gwt-DisclosurePanel * <dd>the panel's primary style * <dt>.gwt-DisclosurePanel-open * <dd> dependent style set when panel is open * <dt>.gwt-DisclosurePanel-closed * <dd> dependent style set when panel is closed * </dl> * <p> * <img class='gallery' src='doc-files/DisclosurePanel.png'/> * </p> * * <p> * The header and content sections can be easily selected using css with a child * selector:<br/> * .gwt-DisclosurePanel-open .header { ... } * </p> * <h3>Use in UiBinder Templates</h3> * <p> * DisclosurePanel elements in * {@link com.google.gwt.uibinder.client.UiBinder UiBinder} templates can * have one widget child and one of two types of header elements. A * <g:header> element can hold text (not html), or a <g:customHeader> element * can hold a widget. (Note that the tags of the header elements are not * capitalized. This is meant to signal that the header is not a runtime object, * and so cannot have a <code>ui:field</code> attribute.) * <p> * For example:<pre> * <g:DisclosurePanel> * <g:header>Text header</g:header> * <g:Label>Widget body</g:Label> * </g:DisclosurePanel> * * <g:DisclosurePanel> * <g:customHeader> * <g:Label>Widget header</g:Label> * </g:customHeader> * <g:Label>Widget body</g:Label> * </g:DisclosurePanel> * </pre> */ @SuppressWarnings("deprecation") public final class DisclosurePanel extends Composite implements FiresDisclosureEvents, HasWidgets.ForIsWidget, HasAnimation, HasOpenHandlers<DisclosurePanel>, HasCloseHandlers<DisclosurePanel> { interface DefaultImages extends ClientBundle { @ImageOptions(flipRtl = true) ImageResource disclosurePanelClosed(); ImageResource disclosurePanelOpen(); } private static final DefaultImages DEFAULT_IMAGES = GWT.create(DefaultImages.class); /** * 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 { 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); } @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); setOpen(!isOpen); } } } /** * An {@link Animation} used to open the content. */ private static class ContentAnimation extends Animation { /** * Whether the item is being opened or closed. */ private boolean opening; /** * The {@link DisclosurePanel} being affected. */ private DisclosurePanel curPanel; /** * Open or close the content. * * @param panel the panel to open or close * @param animate true to animate, false to open instantly */ public void setOpen(DisclosurePanel panel, boolean animate) { // Immediately complete previous open cancel(); // Open the new item if (animate) { curPanel = panel; opening = panel.isOpen; run(ANIMATION_DURATION); } else { panel.contentWrapper.setVisible(panel.isOpen); if (panel.isOpen) { // Special treatment on the visible case to ensure LazyPanel works panel.getContent().setVisible(true); } } } @Override protected void onComplete() { if (!opening) { curPanel.contentWrapper.setVisible(false); } DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "height", "auto"); curPanel = null; } @Override protected void onStart() { super.onStart(); if (opening) { curPanel.contentWrapper.setVisible(true); // Special treatment on the visible case to ensure LazyPanel works curPanel.getContent().setVisible(true); } } @Override protected void onUpdate(double progress) { int scrollHeight = DOM.getElementPropertyInt( curPanel.contentWrapper.getElement(), "scrollHeight"); int height = (int) (progress * scrollHeight); if (!opening) { height = scrollHeight - height; } height = Math.max(height, 1); DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "height", height + "px"); DOM.setStyleAttribute(curPanel.contentWrapper.getElement(), "width", "auto"); } } /** * The default header widget used within a {@link DisclosurePanel}. */ private class DefaultHeader extends Widget implements HasText, OpenHandler<DisclosurePanel>, CloseHandler<DisclosurePanel> { /** * imageTD holds the image for the icon, not null. labelTD holds the text * for the label. */ private final Element labelTD; private final Image iconImage; private final Imager imager; private DefaultHeader(final DisclosurePanelImages images, String text) { this(new Imager() { public Image makeImage() { return images.disclosurePanelClosed().createImage(); } public void updateImage(boolean open, Image image) { if (open) { images.disclosurePanelOpen().applyTo(image); } else { images.disclosurePanelClosed().applyTo(image); } } }, text); } private DefaultHeader(Imager imager, String text) { this.imager = imager; iconImage = imager.makeImage(); // I do not need any Widgets here, just a DOM structure. Element root = DOM.createTable(); Element tbody = DOM.createTBody(); Element tr = DOM.createTR(); final Element imageTD = DOM.createTD(); labelTD = DOM.createTD(); setElement(root); DOM.appendChild(root, tbody); DOM.appendChild(tbody, tr); DOM.appendChild(tr, imageTD); DOM.appendChild(tr, labelTD); // set image TD to be same width as image. DOM.setElementProperty(imageTD, "align", "center"); DOM.setElementProperty(imageTD, "valign", "middle"); DOM.setStyleAttribute(imageTD, "width", iconImage.getWidth() + "px"); DOM.appendChild(imageTD, iconImage.getElement()); setText(text); addOpenHandler(this); addCloseHandler(this); setStyle(); } private DefaultHeader(final ImageResource openImage, final ImageResource closedImage, String text) { this(new Imager() { public Image makeImage() { return new Image(closedImage); } public void updateImage(boolean open, Image image) { if (open) { image.setResource(openImage); } else { image.setResource(closedImage); } } }, text); } public final String getText() { return DOM.getInnerText(labelTD); } public final void onClose(CloseEvent<DisclosurePanel> event) { setStyle(); } public final void onOpen(OpenEvent<DisclosurePanel> event) { setStyle(); } public final void setText(String text) { DOM.setInnerText(labelTD, text); } private void setStyle() { imager.updateImage(isOpen, iconImage); } } private interface Imager { Image makeImage(); void updateImage(boolean open, Image image); } /** * The duration of the animation. */ private static final int ANIMATION_DURATION = 350; // 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"; /** * The {@link Animation} used to open and close the content. */ private static ContentAnimation contentAnimation; /** * top level widget. The first child will be a reference to {@link #header}. * The second child will be a reference to {@link #contentWrapper}. */ private final VerticalPanel mainPanel = new VerticalPanel(); /** * The wrapper around the content widget. */ private final SimplePanel contentWrapper = new SimplePanel(); /** * holds the header widget. */ private final ClickableHeader header = new ClickableHeader(); private boolean isAnimationEnabled = false; private boolean isOpen = false; /** * Creates an empty DisclosurePanel that is initially closed. */ public DisclosurePanel() { initWidget(mainPanel); mainPanel.add(header); mainPanel.add(contentWrapper); DOM.setStyleAttribute(contentWrapper.getElement(), "padding", "0px"); DOM.setStyleAttribute(contentWrapper.getElement(), "overflow", "hidden"); setStyleName(STYLENAME_DEFAULT); setContentDisplay(false); } /** * Creates a DisclosurePanel with the specified header text, an initial * open/close state and a bundle of images to be used in the default header * widget. * * @param images a bundle that provides disclosure panel specific images * @param headerText the text to be displayed in the header * @param isOpen the initial open/close state of the content panel * * @deprecated use * {@link #DisclosurePanel(ImageResource, ImageResource, String)} * and {@link #setOpen(boolean)} */ @Deprecated public DisclosurePanel(DisclosurePanelImages images, String headerText, boolean isOpen) { this(); setOpen(isOpen); setHeader(new DefaultHeader(images, headerText)); } /** * Creates a DisclosurePanel with the specified header text, an initial * open/close state and a bundle of images to be used in the default header * widget. * * @param openImage the open state image resource * @param closedImage the closed state image resource * @param headerText the text to be displayed in the header */ public DisclosurePanel(ImageResource openImage, ImageResource closedImage, String headerText) { this(); setHeader(new DefaultHeader(openImage, closedImage, headerText)); } /** * Creates a DisclosurePanel that will be initially closed using the specified * text in the header. * * @param headerText the text to be displayed in the header */ public DisclosurePanel(String headerText) { this(DEFAULT_IMAGES.disclosurePanelOpen(), DEFAULT_IMAGES.disclosurePanelClosed(), headerText); } /** * Creates a DisclosurePanel with the specified header text and an initial * open/close state. * * @param headerText the text to be displayed in the header * @param isOpen the initial open/close state of the content panel * @deprecated use {@link #DisclosurePanel(String)} and * {@link #setOpen(boolean)} */ @Deprecated public DisclosurePanel(String headerText, boolean isOpen) { this(DEFAULT_IMAGES.disclosurePanelOpen(), DEFAULT_IMAGES.disclosurePanelClosed(), headerText); this.setOpen(isOpen); } /** * Creates a DisclosurePanel that will be initially closed using a widget as * the header. * * @param header the widget to be used as a header * @deprecated use {@link #DisclosurePanel()} and {@link #setHeader(Widget)} */ @Deprecated public DisclosurePanel(Widget header) { this(); setHeader(header); } /** * Creates a DisclosurePanel using a widget as the header and an initial * open/close state. * * @param header the widget to be used as a header * @param isOpen the initial open/close state of the content panel * @deprecated use {@link #DisclosurePanel()}, {@link #setOpen(boolean)} and * {@link #setHeader(Widget)} instead */ @Deprecated public DisclosurePanel(Widget header, boolean isOpen) { this(); setHeader(header); setOpen(isOpen); } public void add(Widget w) { if (this.getContent() == null) { setContent(w); } else { throw new IllegalStateException( "A DisclosurePanel can only contain two Widgets."); } } /** * Overloaded version for IsWidget. * * @see #add(Widget) */ public void add(IsWidget w) { this.add(asWidgetOrNull(w)); } public HandlerRegistration addCloseHandler( CloseHandler<DisclosurePanel> handler) { return addHandler(handler, CloseEvent.getType()); } /** * Attaches an event handler to the panel to receive {@link DisclosureEvent} * notification. * * @param handler the handler to be added (should not be null) * @deprecated Use {@link DisclosurePanel#addOpenHandler(OpenHandler)} and * {@link DisclosurePanel#addCloseHandler(CloseHandler)} instead */ @Deprecated public void addEventHandler(final DisclosureHandler handler) { ListenerWrapper.WrappedOldDisclosureHandler.add(this, handler); } public HandlerRegistration addOpenHandler(OpenHandler<DisclosurePanel> handler) { return addHandler(handler, OpenEvent.getType()); } public void clear() { setContent(null); } /** * Gets the widget that was previously set in {@link #setContent(Widget)}. * * @return the panel's current content widget */ public Widget getContent() { return contentWrapper.getWidget(); } /** * Gets the widget that is currently being used as a header. * * @return the widget currently being used as a header */ public Widget getHeader() { return header.getWidget(); } /** * Gets a {@link HasText} instance to provide access to the headers's text, if * the header widget does provide such access. * * @return a reference to the header widget if it implements {@link HasText}, * <code>null</code> otherwise */ public HasText getHeaderTextAccessor() { Widget widget = header.getWidget(); return (widget instanceof HasText) ? (HasText) widget : null; } public boolean isAnimationEnabled() { return isAnimationEnabled; } /** * Determines whether the panel is open. * * @return <code>true</code> if panel is in open state */ public boolean isOpen() { return isOpen; } public Iterator<Widget> iterator() { return WidgetIterators.createWidgetIterator(this, new Widget[] {getContent()}); } public boolean remove(Widget w) { if (w == getContent()) { setContent(null); return true; } return false; } /** * Overloaded version for IsWidget. * * @see #remove(Widget) */ public boolean remove(IsWidget w) { return this.remove(asWidgetOrNull(w)); } /** * Removes an event handler from the panel. * * @param handler the handler to be removed * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the * object returned by an add*Handler method instead */ @Deprecated public void removeEventHandler(DisclosureHandler handler) { ListenerWrapper.WrappedOldDisclosureHandler.remove(this, handler); } 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 */ public void setContent(Widget content) { final Widget currentContent = getContent(); // Remove existing content widget. if (currentContent != null) { contentWrapper.setWidget(null); currentContent.removeStyleName(STYLENAME_CONTENT); } // Add new content widget if != null. if (content != null) { contentWrapper.setWidget(content); content.addStyleName(STYLENAME_CONTENT); setContentDisplay(false); } } /** * 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(true); fireEvent(); } } /** * <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 fireEvent() { if (isOpen) { OpenEvent.fire(this, this); } else { CloseEvent.fire(this, this); } } 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) { if (contentAnimation == null) { contentAnimation = new ContentAnimation(); } contentAnimation.setOpen(this, animate && isAnimationEnabled); } } }