blob: 8015dd745885982dba0596c60bbf1eae4e2021f6 [file] [log] [blame]
/*
* 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.dom.client.Element;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.safehtml.shared.annotations.SuppressIsSafeHtmlCastCheck;
import com.google.gwt.user.client.DOM;
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);
table.setPropertyInt("cellSpacing", 0);
table.setPropertyInt("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
*/
@SuppressIsSafeHtmlCastCheck
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, @IsSafeHtml 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);
tdh.setPropertyInt("__owner", hashCode());
tdh.setPropertyString("height", "1px");
// body styling
setStyleName(tdb, DEFAULT_STYLENAME + "Content", true);
tdb.setPropertyString("height", "100%");
tdb.setPropertyString("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
*/
@SuppressIsSafeHtmlCastCheck
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, @IsSafeHtml 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) {
getHeaderTextElem(headerElem).setInnerHTML(text);
} else {
getHeaderTextElem(headerElem).setInnerText(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;
}
/**
* Adds the {@code styleName} on the {@code <tr>} for the header specified by {@code index}.
*
* @param index the index of the header row to apply to the style to
* @param styleName the name of the class to add
*/
public void addHeaderStyleName(int index, String styleName) {
if (index >= getWidgetCount()) {
return;
}
Element tr = DOM.getChild(body, index * 2);
setStyleName(tr, styleName, true /* add */);
}
/**
* Removes the {@code styleName} off the {@code <tr>} for the header specified by {@code index}.
*
* @param index the index of the header row to remove the style from
* @param styleName the name of the class to remove
*/
public void removeHeaderStyleName(int index, String styleName) {
if (index >= getWidgetCount()) {
return;
}
Element tr = DOM.getChild(body, index * 2);
setStyleName(tr, styleName, false /* remove */);
}
private int findDividerIndex(Element elem) {
while (elem != null && elem != getElement()) {
String expando = elem.getPropertyString("__index");
if (expando != null) {
// Make sure it belongs to me!
int ownerHash = elem.getPropertyInt("__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);
body.removeChild(tr);
tr = DOM.getChild(body, rowIndex);
body.removeChild(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);
childTD.setPropertyInt("__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);
}
}
}
}