/* * 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.safehtml.shared.SafeHtml; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; /** * 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 quirks mode. If your application is in * Standards Mode, use {@link StackLayoutPanel} instead. * </p> * * <p> * <img class='gallery' src='doc-files/StackPanel.png'/> * </p> * <h3>CSS Style Rules</h3> * <ul class='css'> * <li>.gwt-StackPanel { the panel itself }</li> * <li>.gwt-StackPanel .gwt-StackPanelItem { unselected items }</li> * <li>.gwt-StackPanel .gwt-StackPanelItem-selected { selected items }</li> * <li>.gwt-StackPanel .gwt-StackPanelContent { the wrapper around the contents * of the item }</li> * </ul> * <p> * <h3>Example</h3> * {@example com.google.gwt.examples.StackPanelExample} * </p> * * @see StackLayoutPanel */ public class StackPanel extends ComplexPanel implements InsertPanel.ForIsWidget { private static final String DEFAULT_STYLENAME = "gwt-StackPanel"; private static final String DEFAULT_ITEM_STYLENAME = DEFAULT_STYLENAME + "Item"; private Element body; private int visibleStack = -1; /** * Creates an empty stack panel. */ public StackPanel() { Element table = DOM.createTable(); setElement(table); body = DOM.createTBody(); DOM.appendChild(table, body); DOM.setElementPropertyInt(table, "cellSpacing", 0); DOM.setElementPropertyInt(table, "cellPadding", 0); DOM.sinkEvents(table, Event.ONCLICK); setStyleName(DEFAULT_STYLENAME); } @Override public void add(Widget w) { insert(w, getWidgetCount()); } /** * Adds a new child with the given widget and header. * * @param w the widget to be added * @param stackText the header text associated with this widget */ public void add(Widget w, String stackText) { add(w, stackText, false); } /** * Adds a new child with the given widget and header, optionally interpreting * the header as HTML. * * @param w the widget to be added * @param stackHtml the header html associated with this widget */ public void add(Widget w, SafeHtml stackHtml) { add(w, stackHtml.asString(), true); } /** * Adds a new child with the given widget and header, optionally interpreting * the header as HTML. * * @param w the widget to be added * @param stackText the header text associated with this widget * @param asHTML <code>true</code> to treat the specified text as HTML */ public void add(Widget w, String stackText, boolean asHTML) { add(w); setStackText(getWidgetCount() - 1, stackText, asHTML); } /** * Gets the currently selected child index. * * @return selected child */ public int getSelectedIndex() { return visibleStack; } public void insert(IsWidget w, int beforeIndex) { insert(asWidgetOrNull(w), beforeIndex); } public void insert(Widget w, int beforeIndex) { // header Element trh = DOM.createTR(); Element tdh = DOM.createTD(); DOM.appendChild(trh, tdh); DOM.appendChild(tdh, createHeaderElem()); // body Element trb = DOM.createTR(); Element tdb = DOM.createTD(); DOM.appendChild(trb, tdb); // DOM indices are 2x logical indices; 2 dom elements per stack item beforeIndex = adjustIndex(w, beforeIndex); int effectiveIndex = beforeIndex * 2; // this ordering puts the body below the header DOM.insertChild(body, trb, effectiveIndex); DOM.insertChild(body, trh, effectiveIndex); // header styling setStyleName(tdh, DEFAULT_ITEM_STYLENAME, true); DOM.setElementPropertyInt(tdh, "__owner", hashCode()); DOM.setElementProperty(tdh, "height", "1px"); // body styling setStyleName(tdb, DEFAULT_STYLENAME + "Content", true); DOM.setElementProperty(tdb, "height", "100%"); DOM.setElementProperty(tdb, "vAlign", "top"); // Now that the DOM is connected, call insert (this ensures that onLoad() is // not fired until the child widget is attached to the DOM). insert(w, tdb, beforeIndex, false); // Update indices of all elements to the right. updateIndicesFrom(beforeIndex); // Correct visible stack for new location. if (visibleStack == -1) { showStack(0); } else { setStackVisible(beforeIndex, false); if (visibleStack >= beforeIndex) { ++visibleStack; } // Reshow the stack to apply style names setStackVisible(visibleStack, true); } } @Override public void onBrowserEvent(Event event) { if (DOM.eventGetType(event) == Event.ONCLICK) { Element target = DOM.eventGetTarget(event); int index = findDividerIndex(target); if (index != -1) { showStack(index); } } super.onBrowserEvent(event); } @Override public boolean remove(int index) { return remove(getWidget(index), index); } @Override public boolean remove(Widget child) { return remove(child, getWidgetIndex(child)); } /** * Sets the text associated with a child by its index. * * @param index the index of the child whose text is to be set * @param text the text to be associated with it */ public void setStackText(int index, String text) { setStackText(index, text, false); } /** * Sets the html associated with a child by its index. * * @param index the index of the child whose text is to be set * @param html the html to be associated with it */ public void setStackText(int index, SafeHtml html) { setStackText(index, html.asString(), true); } /** * Sets the text associated with a child by its index. * * @param index the index of the child whose text is to be set * @param text the text to be associated with it * @param asHTML <code>true</code> to treat the specified text as HTML */ public void setStackText(int index, String text, boolean asHTML) { if (index >= getWidgetCount()) { return; } Element tdWrapper = DOM.getChild(DOM.getChild(body, index * 2), 0); Element headerElem = DOM.getFirstChild(tdWrapper); if (asHTML) { DOM.setInnerHTML(getHeaderTextElem(headerElem), text); } else { DOM.setInnerText(getHeaderTextElem(headerElem), text); } } /** * Shows the widget at the specified child index. * * @param index the index of the child to be shown */ public void showStack(int index) { if ((index >= getWidgetCount()) || (index < 0) || (index == visibleStack)) { return; } if (visibleStack >= 0) { setStackVisible(visibleStack, false); } visibleStack = index; setStackVisible(visibleStack, true); } /** * <b>Affected Elements:</b> * <ul> * <li>-text# = The element around the header at the specified index.</li> * <li>-text-wrapper# = The element around the header at the specified index.</li> * <li>-content# = The element around the body at the specified index.</li> * </ul> * * @see UIObject#onEnsureDebugId(String) */ @Override protected void onEnsureDebugId(String baseID) { super.onEnsureDebugId(baseID); int numHeaders = DOM.getChildCount(body) >> 1; for (int i = 0; i < numHeaders; i++) { Element tdWrapper = DOM.getFirstChild(DOM.getChild(body, 2 * i)); Element headerElem = DOM.getFirstChild(tdWrapper); Element bodyElem = DOM.getFirstChild(DOM.getChild(body, 2 * i + 1)); ensureDebugId(tdWrapper, baseID, "text-wrapper" + i); ensureDebugId(bodyElem, baseID, "content" + i); ensureDebugId(getHeaderTextElem(headerElem), baseID, "text" + i); } } /** * Returns a header element. */ Element createHeaderElem() { return DOM.createDiv(); } /** * Get the element that holds the header text given the header element created * by #createHeaderElement. * * @param headerElem the header element * @return the element around the header text */ Element getHeaderTextElem(Element headerElem) { return headerElem; } private int findDividerIndex(Element elem) { while (elem != null && elem != getElement()) { String expando = DOM.getElementProperty(elem, "__index"); if (expando != null) { // Make sure it belongs to me! int ownerHash = DOM.getElementPropertyInt(elem, "__owner"); if (ownerHash == hashCode()) { // Yes, it's mine. return Integer.parseInt(expando); } else { // It must belong to some nested StackPanel. return -1; } } elem = DOM.getParent(elem); } return -1; } private boolean remove(Widget child, int index) { // Make sure to call this before disconnecting the DOM. boolean removed = super.remove(child); if (removed) { // Calculate which internal table elements to remove. int rowIndex = 2 * index; Element tr = DOM.getChild(body, rowIndex); DOM.removeChild(body, tr); tr = DOM.getChild(body, rowIndex); DOM.removeChild(body, tr); // Correct visible stack for new location. if (visibleStack == index) { visibleStack = -1; } else if (visibleStack > index) { --visibleStack; } // Update indices of all elements to the right. updateIndicesFrom(index); } return removed; } private void setStackContentVisible(int index, boolean visible) { Element tr = DOM.getChild(body, (index * 2) + 1); UIObject.setVisible(tr, visible); getWidget(index).setVisible(visible); } private void setStackVisible(int index, boolean visible) { // Get the first table row containing the widget's selector item. Element tr = DOM.getChild(body, (index * 2)); if (tr == null) { return; } // Style the stack selector item. Element td = DOM.getFirstChild(tr); setStyleName(td, DEFAULT_ITEM_STYLENAME + "-selected", visible); // Show/hide the contained widget. setStackContentVisible(index, visible); // Set the style of the next header Element trNext = DOM.getChild(body, ((index + 1) * 2)); if (trNext != null) { Element tdNext = DOM.getFirstChild(trNext); setStyleName(tdNext, DEFAULT_ITEM_STYLENAME + "-below-selected", visible); } } private void updateIndicesFrom(int beforeIndex) { for (int i = beforeIndex, c = getWidgetCount(); i < c; ++i) { Element childTR = DOM.getChild(body, i * 2); Element childTD = DOM.getFirstChild(childTR); DOM.setElementPropertyInt(childTD, "__index", i); // Update the special style on the first element if (beforeIndex == 0) { setStyleName(childTD, DEFAULT_ITEM_STYLENAME + "-first", true); } else { setStyleName(childTD, DEFAULT_ITEM_STYLENAME + "-first", false); } } } }