Revision: 6397 Author: j...@google.com Date: Fri Oct 16 12:16:25 2009 Log: Initial implementation of TabLayoutPanel. http://gwt-code-reviews.appspot.com/78820 http://code.google.com/p/google-web-toolkit/source/detail?r=6397
Added: /trunk/user/javadoc/com/google/gwt/examples/TabLayoutPanelExample.java /trunk/user/src/com/google/gwt/user/client/ui/TabLayoutPanel.java /trunk/user/test/com/google/gwt/user/client/ui/TabLayoutPanelTest.java Modified: /trunk/user/src/com/google/gwt/user/client/ui/LayoutPanel.java ======================================= --- /dev/null +++ /trunk/user/javadoc/com/google/gwt/examples/TabLayoutPanelExample.java Fri Oct 16 12:16:25 2009 @@ -0,0 +1,43 @@ +/* + * 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.examples; + +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.RootLayoutPanel; +import com.google.gwt.user.client.ui.TabLayoutPanel; + +public class TabLayoutPanelExample implements EntryPoint { + + public void onModuleLoad() { + // Create a three-item tab panel, with the tab area 1.5em tall. + TabLayoutPanel p = new TabLayoutPanel(1.5, Unit.EM); + p.add(new HTML("this"), "[this]"); + p.add(new HTML("that"), "[that]"); + p.add(new HTML("the other"), "[the other]"); + + // Attach the LayoutPanel to the RootLayoutPanel. The latter will listen for + // resize events on the window to ensure that its children are informed of + // possible size changes. + RootLayoutPanel rp = RootLayoutPanel.get(); + rp.add(p); + + // The RootLayoutPanel requires that its layout() method be explicitly + // called for the initial layout to take effect. + rp.layout(); + } +} ======================================= --- /dev/null +++ /trunk/user/src/com/google/gwt/user/client/ui/TabLayoutPanel.java Fri Oct 16 12:16:25 2009 @@ -0,0 +1,449 @@ +/* + * 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.dom.client.Style.Visibility; +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.Layer; + +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 <!DOCTYPE> + * declaration. + * </p> + * + * <p> + * NOTE: This class is still very new, and its interface may change without + * warning. Use at your own risk. + * </p> + * + * <p> + * <h3>Example</h3> + * {...@example com.google.gwt.examples.TabLayoutPanelExample} + * </p> + * + * TODO: + * - Aria, RTL, DebugId + * - Update style mechanism (gwt-Tab, etc. not really sufficient). + */ +public class TabLayoutPanel extends LayoutComposite implements HasWidgets, + RequiresResize, ProvidesResize, IndexedPanel, + HasBeforeSelectionHandlers<Integer>, HasSelectionHandlers<Integer> { + + private static class Tab extends SimplePanel { + private Element anchor; + + public Tab(Widget child) { + super(Document.get().createSpanElement()); + getElement().appendChild(anchor = Document.get().createAnchorElement()); + + setWidget(child); + setStyleName("gwt-Tab"); + anchor.setClassName("gwt-TabInner"); + + // TODO: float:left may not be enough. If there are tabs of differeing + // heights, the shorter ones will top-align, rather than bottom-align, + // which is what we would want. display:inline-block fixes this, but + // needs lots of cross-browser hacks to work properly. + getElement().getStyle().setProperty("float", "left"); + } + + public HandlerRegistration addClickHandler(ClickHandler handler) { + return addDomHandler(handler, ClickEvent.getType()); + } + + public Widget asWidget() { + return this; + } + + public void setSelected(boolean selected) { + if (selected) { + addStyleDependentName("selected"); + } else { + removeStyleDependentName("selected"); + } + } + + @Override + protected com.google.gwt.user.client.Element getContainerElement() { + return anchor.cast(); + } + } + + private WidgetCollection children = new WidgetCollection(this); + private FlowPanel tabBar = new FlowPanel(); + private ArrayList<Tab> tabs = new ArrayList<Tab>(); + private final double tabBarSize; + private final Unit tabBarUnit; + private LayoutPanel panel; + private int selectedIndex = -1; + + /** + * Creates an empty tab panel. + * + * @param tabBarSize the size of the tab bar + * @param tabBarUnit the unit in which the tab bar size is specified + */ + public TabLayoutPanel(double tabBarSize, Unit tabBarUnit) { + this.tabBarSize = tabBarSize; + this.tabBarUnit = tabBarUnit; + + panel = new LayoutPanel(); + initWidget(panel); + + panel.add(tabBar); + Layer layer = panel.getLayer(tabBar); + layer.setLeftRight(0, Unit.PX, 0, Unit.PX); + layer.setTopHeight(0, Unit.PX, tabBarSize, tabBarUnit); + panel.layout(); + + panel.getLayer(tabBar).setChildVerticalPosition(Alignment.END); + + tabBar.setStyleName("gwt-TabLayoutPanelTabs"); + setStyleName("gwt-TabLayoutPanel"); + } + + 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 tabText 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 tabText the text to be shown on its tab + * @param asHtml <code>true</code> to treat the specified text as HTML + */ + public void add(Widget w, String text, boolean asHtml) { + insert(w, 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 clear() { + Iterator<Widget> it = iterator(); + while (it.hasNext()) { + it.next(); + it.remove(); + } + } + + /** + * 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(); + } + + /** + * 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)); + } + + public Widget getWidget(int index) { + checkIndex(index); + return children.get(index); + } + + public int getWidgetCount() { + return children.size(); + } + + public int getWidgetIndex(Widget child) { + return children.indexOf(child); + } + + /** + * 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 w, int beforeIndex) { + insert(w, "", 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 tabText 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 w, String text, boolean asHtml, int beforeIndex) { + Widget contents; + if (asHtml) { + contents = new HTML(text); + } else { + contents = new Label(text); + } + insert(w, 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 tabText the text to be shown on its tab + * @param beforeIndex the index before which it will be inserted + */ + 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); + } + + public Iterator<Widget> iterator() { + return children.iterator(); + } + + public boolean remove(int index) { + if ((index < 0) || (index >= getWidgetCount())) { + return false; + } + + tabBar.remove(index); + panel.remove(children.get(index)); + + children.remove(index); + tabs.remove(index); + + 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 = children.indexOf(w); + if (index == -1) { + return false; + } + + return remove(index); + } + + /** + * Programmatically selects the specified tab. + * + * @param index the index of the tab to be selected + */ + public void selectTab(int index) { + checkIndex(index); + if (index == selectedIndex) { + return; + } + + // Fire the before selection event, giving the recipients a chance to + // cancel the selection. + BeforeSelectionEvent<Integer> event = BeforeSelectionEvent + .fire(this, index); + if ((event != null) && event.isCanceled()) { + return; + } + + // Update the tabs being selected and unselected. + if (selectedIndex != -1) { + Layer layer = panel.getLayer(children.get(selectedIndex)); + layer.getContainerElement().getStyle().setVisibility(Visibility.HIDDEN); + tabs.get(selectedIndex).setSelected(false); + } + + Layer layer = panel.getLayer(children.get(index)); + layer.getContainerElement().getStyle().setVisibility(Visibility.VISIBLE); + tabs.get(index).setSelected(true); + selectedIndex = index; + + // Fire the selection event. + SelectionEvent.fire(this, index); + } + + /** + * Programmatically selects the specified tab. + * + * @param child the child whose tab is to be selected + */ + public void selectTab(Widget child) { + selectTab(getWidgetIndex(child)); + } + + /** + * 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 #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, String text) { + checkIndex(index); + tabs.get(index).setWidget(new HTML(text)); + } + + /** + * 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 children.contains(child); + } + + private void checkIndex(int index) { + assert (index >= 0) && (index < children.size()) : "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--; + } + } + + children.insert(child, beforeIndex); + tabs.add(beforeIndex, tab); + + tabBar.insert(tab.asWidget(), beforeIndex); + tab.addClickHandler(new ClickHandler() { + public void onClick(ClickEvent event) { + selectTab(child); + } + }); + + panel.insert(child, beforeIndex); + layoutChild(child); + + if (selectedIndex == -1) { + selectTab(0); + } + } + + private void layoutChild(Widget child) { + Layer layer = panel.getLayer(child); + layer.setLeftRight(0, Unit.PX, 0, Unit.PX); + layer.setTopBottom(tabBarSize, tabBarUnit, 0, Unit.PX); + layer.getContainerElement().getStyle().setVisibility(Visibility.HIDDEN); + panel.layout(); + } +} ======================================= --- /dev/null +++ /trunk/user/test/com/google/gwt/user/client/ui/TabLayoutPanelTest.java Fri Oct 16 12:16:25 2009 @@ -0,0 +1,175 @@ +/* + * 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.logical.shared.BeforeSelectionEvent; +import com.google.gwt.event.logical.shared.BeforeSelectionHandler; +import com.google.gwt.event.logical.shared.SelectionEvent; +import com.google.gwt.event.logical.shared.SelectionHandler; +import com.google.gwt.junit.client.GWTTestCase; + +import java.util.Iterator; + +/** + * Tests for {...@link TabLayoutPanel}. + */ +public class TabLayoutPanelTest extends GWTTestCase { + + static class Adder implements HasWidgetsTester.WidgetAdder { + public void addChild(HasWidgets container, Widget child) { + ((TabPanel) container).add(child, "foo"); + } + } + + private class TestSelectionHandler implements BeforeSelectionHandler<Integer>, SelectionHandler<Integer> { + private boolean onBeforeFired; + + public void onBeforeSelection(BeforeSelectionEvent<Integer> event) { + onBeforeFired = true; + } + + public void onSelection(SelectionEvent<Integer> event) { + assertTrue(onBeforeFired); + finishTest(); + } + } + + @Override + public String getModuleName() { + return "com.google.gwt.user.DebugTest"; + } + + public void testAttachDetachOrder() { + HasWidgetsTester.testAll(new TabPanel(), new Adder(), true); + } + + public void testInsertMultipleTimes() { + TabLayoutPanel p = new TabLayoutPanel(2, Unit.EM); + + TextBox tb = new TextBox(); + p.add(tb, "Title"); + p.add(tb, "Title"); + p.add(tb, "Title3"); + + assertEquals(1, p.getWidgetCount()); + assertEquals(0, p.getWidgetIndex(tb)); + Iterator<Widget> i = p.iterator(); + assertTrue(i.hasNext()); + assertTrue(tb.equals(i.next())); + assertFalse(i.hasNext()); + + Label l = new Label(); + p.add(l, "Title"); + p.add(l, "Title"); + p.add(l, "Title3"); + assertEquals(2, p.getWidgetCount()); + assertEquals(0, p.getWidgetIndex(tb)); + assertEquals(1, p.getWidgetIndex(l)); + + p.insert(l, "Title", 0); + assertEquals(2, p.getWidgetCount()); + assertEquals(0, p.getWidgetIndex(l)); + assertEquals(1, p.getWidgetIndex(tb)); + + p.insert(l, "Title", 1); + assertEquals(2, p.getWidgetCount()); + assertEquals(0, p.getWidgetIndex(l)); + assertEquals(1, p.getWidgetIndex(tb)); + + p.insert(l, "Title", 2); + assertEquals(2, p.getWidgetCount()); + assertEquals(0, p.getWidgetIndex(tb)); + assertEquals(1, p.getWidgetIndex(l)); + } + + public void testInsertWithHTML() { + TabLayoutPanel p = new TabLayoutPanel(2, Unit.EM); + Label l = new Label(); + p.add(l, "three"); + p.insert(new HTML("<b>hello</b>"), "two", true, 0); + p.insert(new HTML("goodbye"), "one", false, 0); + assertEquals(3, p.getWidgetCount()); + } + + /** + * Tests to ensure that arbitrary widgets can be added/inserted effectively. + */ + public void testInsertWithWidgets() { + TabLayoutPanel p = new TabLayoutPanel(2, Unit.EM); + + TextBox wa = new TextBox(); + CheckBox wb = new CheckBox(); + VerticalPanel wc = new VerticalPanel(); + wc.add(new Label("First")); + wc.add(new Label("Second")); + + p.add(new Label("Content C"), wc); + p.insert(new Label("Content B"), wb, 0); + p.insert(new Label("Content A"), wa, 0); + + // Call these to ensure we don't throw an exception. + assertNotNull(p.getTabWidget(0)); + assertNotNull(p.getTabWidget(1)); + assertNotNull(p.getTabWidget(2)); + assertEquals(3, p.getWidgetCount()); + } + + public void testIterator() { + TabLayoutPanel p = new TabLayoutPanel(2, Unit.EM); + HTML foo = new HTML("foo"); + HTML bar = new HTML("bar"); + HTML baz = new HTML("baz"); + p.add(foo, "foo"); + p.add(bar, "bar"); + p.add(baz, "baz"); + + // Iterate over the entire set and make sure it stops correctly. + Iterator<Widget> it = p.iterator(); + assertTrue(it.hasNext()); + assertTrue(it.next() == foo); + assertTrue(it.hasNext()); + assertTrue(it.next() == bar); + assertTrue(it.hasNext()); + assertTrue(it.next() == baz); + assertFalse(it.hasNext()); + + // Test removing using the iterator. + it = p.iterator(); + it.next(); + it.remove(); + assertTrue(it.next() == bar); + assertTrue(p.getWidgetCount() == 2); + assertTrue(p.getWidget(0) == bar); + assertTrue(p.getWidget(1) == baz); + } + + public void testSelectionEvents() { + TabLayoutPanel p = new TabLayoutPanel(2, Unit.EM); + RootPanel.get().add(p); + + p.add(new Button("foo"), "foo"); + p.add(new Button("bar"), "bar"); + + // Make sure selecting a tab fires both events in the right order. + TestSelectionHandler handler = new TestSelectionHandler(); + p.addBeforeSelectionHandler(handler); + p.addSelectionHandler(handler); + + delayTestFinish(1000); + p.selectTab(1); + } +} ======================================= --- /trunk/user/src/com/google/gwt/user/client/ui/LayoutPanel.java Thu Sep 3 12:28:14 2009 +++ /trunk/user/src/com/google/gwt/user/client/ui/LayoutPanel.java Fri Oct 16 12:16:25 2009 @@ -45,6 +45,8 @@ * <h3>Example</h3> * {...@example com.google.gwt.examples.LayoutPanelExample} * </p> + * + * TODO: implements IndexedPanel (I think) */ public class LayoutPanel extends ComplexPanel implements RequiresLayout, RequiresResize, ProvidesResize { @@ -71,18 +73,7 @@ * @param widget the widget to be added */ public void add(Widget widget) { - // Detach new child. - widget.removeFromParent(); - - // Logical attach. - getChildren().add(widget); - - // Physical attach. - Layer layer = layout.attachChild(widget.getElement(), widget); - widget.setLayoutData(layer); - - // Adopt. - adopt(widget); + insert(widget, getWidgetCount()); } /** @@ -102,6 +93,41 @@ assert child.getParent() == this : "The requested widget is not a child of this panel"; return (Layout.Layer) child.getLayoutData(); } + + /** + * Inserts a widget before the specified index. + * + * <p> + * By default, each child will fill the panel. To build more interesting + * layouts, use {...@link #getLayer(Widget)} to get the {...@link Layout.Layer} + * associated with each child, and set its layout constraints as desired. + * </p> + * + * <p> + * Inserting a widget in this way has no effect on the DOM structure, but can + * be useful for other panels that wrap LayoutPanel to maintain insertion + * order. + * </p> + * + * @param widget the widget to be inserted + * @param beforeIndex the index before which it will be inserted + * @throws IndexOutOfBoundsException if <code>beforeIndex</code> is out of + * range + */ + public void insert(Widget widget, int beforeIndex) { + // Detach new child. + widget.removeFromParent(); + + // Logical attach. + getChildren().insert(widget, beforeIndex); + + // Physical attach. + Layer layer = layout.attachChild(widget.getElement(), widget); + widget.setLayoutData(layer); + + // Adopt. + adopt(widget); + } public void layout() { layout.layout(); --~--~---------~--~----~------------~-------~--~----~ http://groups.google.com/group/Google-Web-Toolkit-Contributors -~----------~----~----~----~------~----~------~--~---