blob: aaf9f867bfbc9a3e2d4aec667bacd2ba67333383 [file] [log] [blame]
/*
* 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.Document;
import com.google.gwt.dom.client.Element;
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.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.Alignment;
import com.google.gwt.layout.client.Layout.AnimationCallback;
import com.google.gwt.resources.client.CommonResources;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.safehtml.shared.annotations.SuppressIsSafeHtmlCastCheck;
import java.util.ArrayList;
import java.util.Iterator;
/**
* A panel that represents a tabbed set of pages, each of which contains another
* widget. Its child widgets are shown as the user selects the various tabs
* associated with them. The tabs can contain arbitrary text, HTML, or widgets.
*
* <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 &lt;!DOCTYPE&gt;
* declaration.
* </p>
*
* <h3>CSS Style Rules</h3>
* <dl>
* <dt>.gwt-TabLayoutPanel
* <dd>the panel itself
* <dt>.gwt-TabLayoutPanel .gwt-TabLayoutPanelTabs
* <dd>the tab bar element
* <dt>.gwt-TabLayoutPanel .gwt-TabLayoutPanelTab
* <dd>an individual tab
* <dt>.gwt-TabLayoutPanel .gwt-TabLayoutPanelTabInner
* <dd>an element nested in each tab (useful for styling)
* <dt>.gwt-TabLayoutPanel .gwt-TabLayoutPanelContent
* <dd>applied to all child content widgets
* </dl>
*
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.TabLayoutPanelExample}
*
* <h3>Use in UiBinder Templates</h3>
* <p>
* A TabLayoutPanel element in a {@link com.google.gwt.uibinder.client.UiBinder
* UiBinder} template must have a <code>barHeight</code> attribute with a double
* value, and may have a <code>barUnit</code> attribute with a
* {@link com.google.gwt.dom.client.Style.Unit Style.Unit} value.
* <code>barUnit</code> defaults to PX.
* <p>
* The children of a TabLayoutPanel element are laid out in &lt;g:tab>
* elements. Each tab can have one widget child and one of two types of header
* elements. A &lt;g:header> element can hold html, or a &lt;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>
* &lt;g:TabLayoutPanel barUnit='EM' barHeight='3'>
* &lt;g:tab>
* &lt;g:header size='7'>&lt;b>HTML&lt;/b> header&lt;/g:header>
* &lt;g:Label>able&lt;/g:Label>
* &lt;/g:tab>
* &lt;g:tab>
* &lt;g:customHeader size='7'>
* &lt;g:Label>Custom header&lt;/g:Label>
* &lt;/g:customHeader>
* &lt;g:Label>baker&lt;/g:Label>
* &lt;/g:tab>
* &lt;/g:TabLayoutPanel>
* </pre>
*/
public class TabLayoutPanel extends ResizeComposite implements HasWidgets,
ProvidesResize, IndexedPanel.ForIsWidget, AnimatedLayout,
HasBeforeSelectionHandlers<Integer>, HasSelectionHandlers<Integer> {
private class Tab extends SimplePanel {
private Element inner;
private boolean replacingWidget;
public Tab(Widget child) {
super(Document.get().createDivElement());
getElement().appendChild(inner = Document.get().createDivElement());
setWidget(child);
setStyleName(TAB_STYLE);
inner.setClassName(TAB_INNER_STYLE);
getElement().addClassName(CommonResources.getInlineBlockStyle());
}
public HandlerRegistration addClickHandler(ClickHandler handler) {
return addDomHandler(handler, ClickEvent.getType());
}
@Override
public boolean remove(Widget w) {
/*
* Removal of items from the TabBar is delegated to the TabLayoutPanel to
* ensure consistency.
*/
int index = tabs.indexOf(this);
if (replacingWidget || index < 0) {
/*
* The tab contents are being replaced, or this tab is no longer in the
* panel, so just remove the widget.
*/
return super.remove(w);
} else {
// Delegate to the TabLayoutPanel.
return TabLayoutPanel.this.remove(index);
}
}
public void setSelected(boolean selected) {
if (selected) {
addStyleDependentName("selected");
} else {
removeStyleDependentName("selected");
}
}
@Override
public void setWidget(Widget w) {
replacingWidget = true;
super.setWidget(w);
replacingWidget = false;
}
@Override
protected com.google.gwt.user.client.Element getContainerElement() {
return inner.cast();
}
}
/**
* This extension of DeckLayoutPanel overrides the public mutator methods to
* prevent external callers from adding to the state of the DeckPanel.
* <p>
* Removal of Widgets is supported so that WidgetCollection.WidgetIterator
* operates as expected.
* </p>
* <p>
* We ensure that the DeckLayoutPanel cannot become of of sync with its
* associated TabBar by delegating all mutations to the TabBar to this
* implementation of DeckLayoutPanel.
* </p>
*/
private class TabbedDeckLayoutPanel extends DeckLayoutPanel {
@Override
public void add(Widget w) {
throw new UnsupportedOperationException(
"Use TabLayoutPanel.add() to alter the DeckLayoutPanel");
}
@Override
public void clear() {
throw new UnsupportedOperationException(
"Use TabLayoutPanel.clear() to alter the DeckLayoutPanel");
}
@Override
public void insert(Widget w, int beforeIndex) {
throw new UnsupportedOperationException(
"Use TabLayoutPanel.insert() to alter the DeckLayoutPanel");
}
@Override
public boolean remove(Widget w) {
/*
* Removal of items from the DeckLayoutPanel is delegated to the
* TabLayoutPanel to ensure consistency.
*/
return TabLayoutPanel.this.remove(w);
}
protected void insertProtected(Widget w, int beforeIndex) {
super.insert(w, beforeIndex);
}
protected void removeProtected(Widget w) {
super.remove(w);
}
}
private static final String CONTENT_CONTAINER_STYLE = "gwt-TabLayoutPanelContentContainer";
private static final String CONTENT_STYLE = "gwt-TabLayoutPanelContent";
private static final String TAB_STYLE = "gwt-TabLayoutPanelTab";
private static final String TAB_INNER_STYLE = "gwt-TabLayoutPanelTabInner";
private static final int BIG_ENOUGH_TO_NOT_WRAP = 16384;
private final TabbedDeckLayoutPanel deckPanel = new TabbedDeckLayoutPanel();
private final FlowPanel tabBar = new FlowPanel();
private final ArrayList<Tab> tabs = new ArrayList<Tab>();
private int selectedIndex = -1;
/**
* Creates an empty tab panel.
*
* @param barHeight the size of the tab bar
* @param barUnit the unit in which the tab bar size is specified
*/
public TabLayoutPanel(double barHeight, Unit barUnit) {
LayoutPanel panel = new LayoutPanel();
initWidget(panel);
// Add the tab bar to the panel.
panel.add(tabBar);
panel.setWidgetLeftRight(tabBar, 0, Unit.PX, 0, Unit.PX);
panel.setWidgetTopHeight(tabBar, 0, Unit.PX, barHeight, barUnit);
panel.setWidgetVerticalPosition(tabBar, Alignment.END);
// Add the deck panel to the panel.
deckPanel.addStyleName(CONTENT_CONTAINER_STYLE);
panel.add(deckPanel);
panel.setWidgetLeftRight(deckPanel, 0, Unit.PX, 0, Unit.PX);
panel.setWidgetTopBottom(deckPanel, barHeight, barUnit, 0, Unit.PX);
// Make the tab bar extremely wide so that tabs themselves never wrap.
// (Its layout container is overflow:hidden)
tabBar.getElement().getStyle().setWidth(BIG_ENOUGH_TO_NOT_WRAP, Unit.PX);
tabBar.setStyleName("gwt-TabLayoutPanelTabs");
setStyleName("gwt-TabLayoutPanel");
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void add(IsWidget w) {
add(asWidgetOrNull(w));
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void add(IsWidget w, IsWidget tab) {
add(asWidgetOrNull(w), asWidgetOrNull(tab));
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void add(IsWidget w, String text) {
add(asWidgetOrNull(w), text);
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void add(IsWidget w, @IsSafeHtml String text, boolean asHtml) {
add(asWidgetOrNull(w), text, asHtml);
}
public void add(Widget w) {
insert(w, getWidgetCount());
}
/**
* Adds a widget to the panel. If the Widget is already attached, it will be
* moved to the right-most index.
*
* @param child the widget to be added
* @param text the text to be shown on its tab
*/
public void add(Widget child, String text) {
insert(child, text, getWidgetCount());
}
/**
* Adds a widget to the panel. If the Widget is already attached, it will be
* moved to the right-most index.
*
* @param child the widget to be added
* @param html the html to be shown on its tab
*/
public void add(Widget child, SafeHtml html) {
add(child, html.asString(), true);
}
/**
* Adds a widget to the panel. If the Widget is already attached, it will be
* moved to the right-most index.
*
* @param child the widget to be added
* @param text the text to be shown on its tab
* @param asHtml <code>true</code> to treat the specified text as HTML
*/
public void add(Widget child, @IsSafeHtml String text, boolean asHtml) {
insert(child, text, asHtml, getWidgetCount());
}
/**
* Adds a widget to the panel. If the Widget is already attached, it will be
* moved to the right-most index.
*
* @param child the widget to be added
* @param tab the widget to be placed in the associated tab
*/
public void add(Widget child, Widget tab) {
insert(child, tab, getWidgetCount());
}
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) {
deckPanel.animate(duration, callback);
}
public void clear() {
Iterator<Widget> it = iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
}
public void forceLayout() {
deckPanel.forceLayout();
}
/**
* Get the duration of the animated transition between tabs.
*
* @return the duration in milliseconds
*/
public int getAnimationDuration() {
return deckPanel.getAnimationDuration();
}
/**
* Gets the index of the currently-selected tab.
*
* @return the selected index, or <code>-1</code> if none is selected.
*/
public int getSelectedIndex() {
return selectedIndex;
}
/**
* Gets the widget in the tab at the given index.
*
* @param index the index of the tab to be retrieved
* @return the tab's widget
*/
public Widget getTabWidget(int index) {
checkIndex(index);
return tabs.get(index).getWidget();
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public Widget getTabWidget(IsWidget child) {
return getTabWidget(asWidgetOrNull(child));
}
/**
* Gets the widget in the tab associated with the given child widget.
*
* @param child the child whose tab is to be retrieved
* @return the tab's widget
*/
public Widget getTabWidget(Widget child) {
checkChild(child);
return getTabWidget(getWidgetIndex(child));
}
/**
* Returns the widget at the given index.
*/
public Widget getWidget(int index) {
return deckPanel.getWidget(index);
}
/**
* Returns the number of tabs and widgets.
*/
public int getWidgetCount() {
return deckPanel.getWidgetCount();
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public int getWidgetIndex(IsWidget child) {
return getWidgetIndex(asWidgetOrNull(child));
}
/**
* Returns the index of the given child, or -1 if it is not a child.
*/
public int getWidgetIndex(Widget child) {
return deckPanel.getWidgetIndex(child);
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void insert(IsWidget child, int beforeIndex) {
insert(asWidgetOrNull(child), beforeIndex);
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void insert(IsWidget child, IsWidget tab, int beforeIndex) {
insert(asWidgetOrNull(child), asWidgetOrNull(tab), beforeIndex);
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void insert(IsWidget child, @IsSafeHtml String text, boolean asHtml, int beforeIndex) {
insert(asWidgetOrNull(child), text, asHtml, beforeIndex);
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void insert(IsWidget child, String text, int beforeIndex) {
insert(asWidgetOrNull(child), text, 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 beforeIndex the index before which it will be inserted
*/
public void insert(Widget child, int beforeIndex) {
insert(child, "", 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 html the html to be shown on its tab
* @param beforeIndex the index before which it will be inserted
*/
public void insert(Widget child, SafeHtml html, int beforeIndex) {
insert(child, html.asString(), true, 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 tab
* @param asHtml <code>true</code> to treat the specified text as HTML
* @param beforeIndex the index before which it will be inserted
*/
public void insert(Widget child, @IsSafeHtml String text, boolean asHtml, int beforeIndex) {
Widget contents;
if (asHtml) {
contents = new HTML(text);
} else {
contents = new Label(text);
}
insert(child, contents, 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 tab
* @param beforeIndex the index before which it will be inserted
*/
@SuppressIsSafeHtmlCastCheck
public void insert(Widget child, String text, int beforeIndex) {
insert(child, text, false, 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 tab the widget to be placed in the associated tab
* @param beforeIndex the index before which it will be inserted
*/
public void insert(Widget child, Widget tab, int beforeIndex) {
insert(child, new Tab(tab), beforeIndex);
}
/**
* Check whether or not transitions slide in vertically or horizontally.
* Defaults to horizontally.
*
* @return true for vertical transitions, false for horizontal
*/
public boolean isAnimationVertical() {
return deckPanel.isAnimationVertical();
}
public Iterator<Widget> iterator() {
return deckPanel.iterator();
}
public boolean remove(int index) {
if ((index < 0) || (index >= getWidgetCount())) {
return false;
}
Widget child = getWidget(index);
tabBar.remove(index);
deckPanel.removeProtected(child);
child.removeStyleName(CONTENT_STYLE);
Tab tab = tabs.remove(index);
tab.getWidget().removeFromParent();
if (index == selectedIndex) {
// If the selected tab is being removed, select the first tab (if there
// is one).
selectedIndex = -1;
if (getWidgetCount() > 0) {
selectTab(0);
}
} else if (index < selectedIndex) {
// If the selectedIndex is greater than the one being removed, it needs
// to be adjusted.
--selectedIndex;
}
return true;
}
public boolean remove(Widget w) {
int index = getWidgetIndex(w);
if (index == -1) {
return false;
}
return remove(index);
}
/**
* Programmatically selects the specified tab and fires events.
*
* @param index the index of the tab to be selected
*/
public void selectTab(int index) {
selectTab(index, true);
}
/**
* Programmatically selects the specified tab.
*
* @param index the index of the tab to be selected
* @param fireEvents true to fire events, false not to
*/
public void selectTab(int index, 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;
}
}
// Update the tabs being selected and unselected.
if (selectedIndex != -1) {
tabs.get(selectedIndex).setSelected(false);
}
deckPanel.showWidget(index);
tabs.get(index).setSelected(true);
selectedIndex = index;
// Fire the selection event.
if (fireEvents) {
SelectionEvent.fire(this, index);
}
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void selectTab(IsWidget child) {
selectTab(asWidgetOrNull(child));
}
/**
* Convenience overload to allow {@link IsWidget} to be used directly.
*/
public void selectTab(IsWidget child, boolean fireEvents) {
selectTab(asWidgetOrNull(child), fireEvents);
}
/**
* Programmatically selects the specified tab and fires events.
*
* @param child the child whose tab is to be selected
*/
public void selectTab(Widget child) {
selectTab(getWidgetIndex(child));
}
/**
* Programmatically selects the specified tab.
*
* @param child the child whose tab is to be selected
* @param fireEvents true to fire events, false not to
*/
public void selectTab(Widget child, boolean fireEvents) {
selectTab(getWidgetIndex(child), fireEvents);
}
/**
* Set the duration of the animated transition between tabs.
*
* @param duration the duration in milliseconds.
*/
public void setAnimationDuration(int duration) {
deckPanel.setAnimationDuration(duration);
}
/**
* Set whether or not transitions slide in vertically or horizontally.
*
* @param isVertical true for vertical transitions, false for horizontal
*/
public void setAnimationVertical(boolean isVertical) {
deckPanel.setAnimationVertical(isVertical);
}
/**
* Sets a tab'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 #setTabHTML(int, SafeHtml)} or
* {@link #setTabText(int, String)} whenever possible.
*
* @param index the index of the tab whose HTML is to be set
* @param html the tab's new HTML contents
*/
public void setTabHTML(int index, @IsSafeHtml String html) {
checkIndex(index);
tabs.get(index).setWidget(new HTML(html));
}
/**
* Sets a tab's HTML contents.
*
* @param index the index of the tab whose HTML is to be set
* @param html the tab's new HTML contents
*/
public void setTabHTML(int index, SafeHtml html) {
setTabHTML(index, html.asString());
}
/**
* Sets a tab's text contents.
*
* @param index the index of the tab whose text is to be set
* @param text the object's new text
*/
public void setTabText(int index, String text) {
checkIndex(index);
tabs.get(index).setWidget(new Label(text));
}
private void checkChild(Widget child) {
assert getWidgetIndex(child) >= 0 : "Child is not a part of this panel";
}
private void checkIndex(int index) {
assert (index >= 0) && (index < getWidgetCount()) : "Index out of bounds";
}
private void insert(final Widget child, Tab tab, int beforeIndex) {
assert (beforeIndex >= 0) && (beforeIndex <= getWidgetCount()) : "beforeIndex out of bounds";
// Check to see if the TabPanel 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--;
}
}
deckPanel.insertProtected(child, beforeIndex);
tabs.add(beforeIndex, tab);
tabBar.insert(tab, beforeIndex);
tab.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
selectTab(child);
}
});
child.addStyleName(CONTENT_STYLE);
if (selectedIndex == -1) {
selectTab(0);
} else if (selectedIndex >= beforeIndex) {
// If we inserted before the currently selected tab, its index has just
// increased.
selectedIndex++;
}
}
}