/* * Copyright 2009 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.dom.client.Style.Unit; 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.dom.client.MouseOutEvent; import com.google.gwt.event.dom.client.MouseOutHandler; import com.google.gwt.event.dom.client.MouseOverEvent; import com.google.gwt.event.dom.client.MouseOverHandler; import com.google.gwt.event.logical.shared.BeforeSelectionEvent; import com.google.gwt.event.logical.shared.BeforeSelectionHandler; import com.google.gwt.event.logical.shared.HasBeforeSelectionHandlers; import com.google.gwt.event.logical.shared.HasSelectionHandlers; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.layout.client.Layout.AnimationCallback; import com.google.gwt.safehtml.shared.SafeHtml; import java.util.ArrayList; import java.util.Iterator; import java.util.NoSuchElementException; /** * A panel that stacks its children vertically, displaying only one at a time, * with a header for each child which the user can click to display. * * <p> * This widget will <em>only</em> work in standards mode, which requires that * the HTML page in which it is run have an explicit <!DOCTYPE> * declaration. * </p> * * <h3>CSS Style Rules</h3> * <dl> * <dt>.gwt-StackLayoutPanel <dd> the panel itself * <dt>.gwt-StackLayoutPanel .gwt-StackLayoutPanelHeader <dd> applied to each * header widget * <dt>.gwt-StackLayoutPanel .gwt-StackLayoutPanelHeader-hovering <dd> applied to each * header widget on mouse hover * <dt>.gwt-StackLayoutPanel .gwt-StackLayoutPanelContent <dd> applied to each * child widget * </dl> * * <p> * <h3>Example</h3> * {@example com.google.gwt.examples.StackLayoutPanelExample} * </p> * * <h3>Use in UiBinder Templates</h3> * <p> * A StackLayoutPanel element in a * {@link com.google.gwt.uibinder.client.UiBinder UiBinder} template may have a * <code>unit</code> attribute with a * {@link com.google.gwt.dom.client.Style.Unit Style.Unit} value (it defaults to * PX). * <p> * The children of a StackLayoutPanel element are laid out in <g:stack> * elements. Each stack can have one widget child and one of two types of header * elements. A <g:header> element can hold 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 head is not a runtime object, * and so cannot have a <code>ui:field</code> attribute.) * <p> * For example: * * <pre> * <g:StackLayoutPanel unit='PX'> * <g:stack> * <g:header size='3'><b>HTML</b> header</g:header> * <g:Label>able</g:Label> * </g:stack> * <g:stack> * <g:customHeader size='3'> * <g:Label>Custom header</g:Label> * </g:customHeader> * <g:Label>baker</g:Label> * </g:stack> * </g:StackLayoutPanel> * </pre> */ public class StackLayoutPanel extends ResizeComposite implements HasWidgets, ProvidesResize, IndexedPanel.ForIsWidget, AnimatedLayout, HasBeforeSelectionHandlers<Integer>, HasSelectionHandlers<Integer> { private class Header extends Composite implements HasClickHandlers { public Header(Widget child) { initWidget(child); } public HandlerRegistration addClickHandler(ClickHandler handler) { return this.addDomHandler(handler, ClickEvent.getType()); } public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) { return this.addDomHandler(handler, MouseOutEvent.getType()); } public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) { return this.addDomHandler(handler, MouseOverEvent.getType()); } } private static class LayoutData { public double headerSize; public Header header; public Widget widget; public LayoutData(Widget widget, Header header, double headerSize) { this.widget = widget; this.header = header; this.headerSize = headerSize; } } private static final String WIDGET_STYLE = "gwt-StackLayoutPanel"; private static final String CONTENT_STYLE = "gwt-StackLayoutPanelContent"; private static final String HEADER_STYLE = "gwt-StackLayoutPanelHeader"; private static final String HEADER_STYLE_HOVERING = "gwt-StackLayoutPanelHeader-hovering"; private static final int ANIMATION_TIME = 250; private int animationDuration = ANIMATION_TIME; private LayoutPanel layoutPanel; private final Unit unit; private final ArrayList<LayoutData> layoutData = new ArrayList<LayoutData>(); private int selectedIndex = -1; /** * Creates an empty stack panel. * * @param unit the unit to be used for layout */ public StackLayoutPanel(Unit unit) { this.unit = unit; initWidget(layoutPanel = new LayoutPanel()); setStyleName(WIDGET_STYLE); } public void add(Widget w) { assert false : "Single-argument add() is not supported for this widget"; } /** * Adds a child widget to this stack, along with a widget representing the * stack header. * * @param widget the child widget to be added * @param header the html to be shown on its header * @param headerSize the size of the header widget */ public void add(final Widget widget, SafeHtml header, double headerSize) { add(widget, header.asString(), true, headerSize); } /** * Adds a child widget to this stack, along with a widget representing the * stack header. * * @param widget the child widget to be added * @param header the text to be shown on its header * @param asHtml <code>true</code> to treat the specified text as HTML * @param headerSize the size of the header widget */ public void add(final Widget widget, String header, boolean asHtml, double headerSize) { insert(widget, header, asHtml, headerSize, getWidgetCount()); } /** * Overloaded version for IsWidget. * * @see #add(Widget,String,boolean,double) */ public void add(final IsWidget widget, String header, boolean asHtml, double headerSize) { this.add(widget.asWidget(), header, asHtml, headerSize); } /** * Adds a child widget to this stack, along with a widget representing the * stack header. * * @param widget the child widget to be added * @param header the text to be shown on its header * @param headerSize the size of the header widget */ public void add(final Widget widget, String header, double headerSize) { insert(widget, header, headerSize, getWidgetCount()); } /** * Adds a child widget to this stack, along with a widget representing the * stack header. * * @param widget the child widget to be added * @param header the header widget * @param headerSize the size of the header widget */ public void add(final Widget widget, Widget header, double headerSize) { insert(widget, header, headerSize, getWidgetCount()); } /** * Overloaded version for IsWidget. * * @see #add(Widget,Widget,double) */ public void add(final IsWidget widget, IsWidget header, double headerSize) { this.add(widget.asWidget(), header.asWidget(), headerSize); } public HandlerRegistration addBeforeSelectionHandler( BeforeSelectionHandler<Integer> handler) { return addHandler(handler, BeforeSelectionEvent.getType()); } public HandlerRegistration addSelectionHandler( SelectionHandler<Integer> handler) { return addHandler(handler, SelectionEvent.getType()); } public void animate(int duration) { animate(duration, null); } public void animate(int duration, AnimationCallback callback) { // Don't try to animate zero widgets. if (layoutData.size() == 0) { if (callback != null) { callback.onAnimationComplete(); } return; } double top = 0, bottom = 0; int i = 0; for (; i < layoutData.size(); ++i) { LayoutData data = layoutData.get(i); layoutPanel.setWidgetTopHeight(data.header, top, unit, data.headerSize, unit); top += data.headerSize; layoutPanel.setWidgetTopHeight(data.widget, top, unit, 0, unit); if (i == selectedIndex) { break; } } for (int j = layoutData.size() - 1; j > i; --j) { LayoutData data = layoutData.get(j); layoutPanel.setWidgetBottomHeight(data.header, bottom, unit, data.headerSize, unit); layoutPanel.setWidgetBottomHeight(data.widget, bottom, unit, 0, unit); bottom += data.headerSize; } LayoutData data = layoutData.get(selectedIndex); layoutPanel.setWidgetTopBottom(data.widget, top, unit, bottom, unit); layoutPanel.animate(duration, callback); } public void clear() { layoutPanel.clear(); layoutData.clear(); selectedIndex = -1; } public void forceLayout() { layoutPanel.forceLayout(); } /** * Get the duration of the animated transition between children. * * @return the duration in milliseconds */ public int getAnimationDuration() { return animationDuration; } /** * Gets the widget in the stack header at the given index. * * @param index the index of the stack header to be retrieved * @return the header widget */ public Widget getHeaderWidget(int index) { checkIndex(index); return layoutData.get(index).header.getWidget(); } /** * Gets the widget in the stack header associated with the given child widget. * * @param child the child whose stack header is to be retrieved * @return the header widget */ public Widget getHeaderWidget(Widget child) { checkChild(child); return getHeaderWidget(getWidgetIndex(child)); } /** * Gets the currently-selected index. * * @return the selected index, or <code>-1</code> if none is selected */ public int getVisibleIndex() { return selectedIndex; } /** * Gets the currently-selected widget. * * @return the selected widget, or <code>null</code> if none exist */ public Widget getVisibleWidget() { if (selectedIndex == -1) { return null; } return getWidget(selectedIndex); } public Widget getWidget(int index) { return layoutPanel.getWidget(index * 2 + 1); } public int getWidgetCount() { return layoutPanel.getWidgetCount() / 2; } public int getWidgetIndex(IsWidget child) { return getWidgetIndex(asWidgetOrNull(child)); } public int getWidgetIndex(Widget child) { int index = layoutPanel.getWidgetIndex(child); if (index == -1) { return index; } return (index - 1) / 2; } /** * Inserts a widget into the panel. If the Widget is already attached, it will * be moved to the requested index. * * @param child the widget to be added * @param html the safe html to be shown on its header * @param headerSize the size of the header widget * @param beforeIndex the index before which it will be inserted */ public void insert(Widget child, SafeHtml html, double headerSize, int beforeIndex) { insert(child, html.asString(), true, headerSize, beforeIndex); } /** * Inserts a widget into the panel. If the Widget is already attached, it will * be moved to the requested index. * * @param child the widget to be added * @param text the text to be shown on its header * @param asHtml <code>true</code> to treat the specified text as HTML * @param headerSize the size of the header widget * @param beforeIndex the index before which it will be inserted */ public void insert(Widget child, String text, boolean asHtml, double headerSize, int beforeIndex) { HTML contents = new HTML(); if (asHtml) { contents.setHTML(text); } else { contents.setText(text); } insert(child, contents, headerSize, beforeIndex); } /** * Inserts a widget into the panel. If the Widget is already attached, it will * be moved to the requested index. * * @param child the widget to be added * @param text the text to be shown on its header * @param headerSize the size of the header widget * @param beforeIndex the index before which it will be inserted */ public void insert(Widget child, String text, double headerSize, int beforeIndex) { insert(child, text, false, headerSize, beforeIndex); } /** * Inserts a widget into the panel. If the Widget is already attached, it will * be moved to the requested index. * * @param child the widget to be added * @param header the widget to be placed in the associated header * @param headerSize the size of the header widget * @param beforeIndex the index before which it will be inserted */ public void insert(Widget child, Widget header, double headerSize, int beforeIndex) { insert(child, new Header(header), headerSize, beforeIndex); } public Iterator<Widget> iterator() { return new Iterator<Widget>() { int i = 0, last = -1; public boolean hasNext() { return i < layoutData.size(); } public Widget next() { if (!hasNext()) { throw new NoSuchElementException(); } return layoutData.get(last = i++).widget; } public void remove() { if (last < 0) { throw new IllegalStateException(); } StackLayoutPanel.this.remove(layoutData.get(last).widget); i = last; last = -1; } }; } @Override public void onResize() { layoutPanel.onResize(); } public boolean remove(int index) { return remove(getWidget(index)); } public boolean remove(Widget child) { if (child.getParent() != layoutPanel) { return false; } // Find the layoutData associated with this widget and remove it. for (int i = 0; i < layoutData.size(); ++i) { LayoutData data = layoutData.get(i); if (data.widget == child) { layoutPanel.remove(data.header); layoutPanel.remove(data.widget); data.header.removeStyleName(HEADER_STYLE); data.widget.removeStyleName(CONTENT_STYLE); layoutData.remove(i); if (selectedIndex == i) { selectedIndex = -1; if (layoutData.size() > 0) { showWidget(layoutData.get(0).widget); } } else { if (i <= selectedIndex) { selectedIndex--; } animate(animationDuration); } return true; } } return false; } /** * Set the duration of the animated transition between children. * * @param duration the duration in milliseconds. */ public void setAnimationDuration(int duration) { this.animationDuration = duration; } /** * Sets a stack header's HTML contents. * * Use care when setting an object's HTML; it is an easy way to expose * script-based security problems. Consider using * {@link #setHeaderHTML(int, SafeHtml)} or * {@link #setHeaderText(int, String)} whenever possible. * * @param index the index of the header whose HTML is to be set * @param html the header's new HTML contents */ public void setHeaderHTML(int index, String html) { checkIndex(index); LayoutData data = layoutData.get(index); Widget headerWidget = data.header.getWidget(); assert headerWidget instanceof HasHTML : "Header widget does not implement HasHTML"; ((HasHTML) headerWidget).setHTML(html); } /** * Sets a stack header's HTML contents. * * @param index the index of the header whose HTML is to be set * @param html the header's new HTML contents */ public void setHeaderHTML(int index, SafeHtml html) { setHeaderHTML(index, html.asString()); } /** * Sets a stack header's text contents. * * @param index the index of the header whose text is to be set * @param text the object's new text */ public void setHeaderText(int index, String text) { checkIndex(index); LayoutData data = layoutData.get(index); Widget headerWidget = data.header.getWidget(); assert headerWidget instanceof HasText : "Header widget does not implement HasText"; ((HasText) headerWidget).setText(text); } /** * Shows the widget at the specified index and fires events. * * @param index the index of the child widget to be shown. */ public void showWidget(int index) { showWidget(index, true); } /** * Shows the widget at the specified index. * * @param index the index of the child widget to be shown. * @param fireEvents true to fire events, false not to */ public void showWidget(int index, boolean fireEvents) { checkIndex(index); showWidget(index, animationDuration, fireEvents); } /** * Shows the specified widget and fires events. * * @param child the child widget to be shown. */ public void showWidget(Widget child) { showWidget(getWidgetIndex(child)); } /** * Shows the specified widget. * * @param child the child widget to be shown. * @param fireEvents true to fire events, false not to */ public void showWidget(Widget child, boolean fireEvents) { showWidget(getWidgetIndex(child), animationDuration, fireEvents); } @Override protected void onLoad() { // When the widget becomes attached, update its layout. animate(0); } private void checkChild(Widget child) { assert layoutPanel.getChildren().contains(child); } private void checkIndex(int index) { assert (index >= 0) && (index < getWidgetCount()) : "Index out of bounds"; } private void insert(final Widget child, final Header header, double headerSize, int beforeIndex) { assert (beforeIndex >= 0) && (beforeIndex <= getWidgetCount()) : "beforeIndex out of bounds"; // Check to see if the StackPanel already contains the Widget. If so, // remove it and see if we need to shift the position to the left. int idx = getWidgetIndex(child); if (idx != -1) { remove(child); if (idx < beforeIndex) { beforeIndex--; } } int widgetIndex = beforeIndex * 2; layoutPanel.insert(child, widgetIndex); layoutPanel.insert(header, widgetIndex); layoutPanel.setWidgetLeftRight(header, 0, Unit.PX, 0, Unit.PX); layoutPanel.setWidgetLeftRight(child, 0, Unit.PX, 0, Unit.PX); LayoutData data = new LayoutData(child, header, headerSize); layoutData.add(beforeIndex, data); header.addStyleName(HEADER_STYLE); child.addStyleName(CONTENT_STYLE); header.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { showWidget(child); } }); header.addMouseOutHandler(new MouseOutHandler() { public void onMouseOut(MouseOutEvent event) { header.removeStyleName(HEADER_STYLE_HOVERING); } }); header.addMouseOverHandler(new MouseOverHandler() { public void onMouseOver(MouseOverEvent event) { header.addStyleName(HEADER_STYLE_HOVERING); } }); if (selectedIndex == -1) { // If there's no visible widget, display the first one. The layout will // be updated onLoad(). showWidget(0); } else if (beforeIndex <= selectedIndex) { // If we inserted an item before the selected index, increment it. selectedIndex++; } // If the widget is already attached, we must call animate() to update the // layout (if it's not yet attached, then onLoad() will do this). if (isAttached()) { animate(animationDuration); } } private void showWidget(int index, final int duration, boolean fireEvents) { checkIndex(index); if (index == selectedIndex) { return; } // Fire the before selection event, giving the recipients a chance to // cancel the selection. if (fireEvents) { BeforeSelectionEvent<Integer> event = BeforeSelectionEvent.fire(this, index); if ((event != null) && event.isCanceled()) { return; } } selectedIndex = index; if (isAttached()) { animate(duration); } // Fire the selection event. if (fireEvents) { SelectionEvent.fire(this, index); } } }