Fixes Issues #596 & #982.
Includes the following changes:
1 - Removes the children field from within TabPanel to rely exclusively on
the contained DeckPanel to maintain the child widgets.
2 - Introduces nested unmodifiable classes for DeckPanel and TabBar to
prevent invariant violating changes from being made though getTabBar()
and getDeckPanel().
3 - Expands TabPanelTest to verify the behavior introduced in 2.
Patch by: bobv
Review by: knorton
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1010 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/TabPanel.java b/user/src/com/google/gwt/user/client/ui/TabPanel.java
index 2ddee07..fae85e8 100644
--- a/user/src/com/google/gwt/user/client/ui/TabPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/TabPanel.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -48,9 +48,121 @@
public class TabPanel extends Composite implements TabListener,
SourcesTabEvents, HasWidgets, IndexedPanel {
- private WidgetCollection children = new WidgetCollection(this);
- private DeckPanel deck = new DeckPanel();
- private TabBar tabBar = new TabBar();
+ /**
+ * This extension of DeckPanel 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 DeckPanel cannot become of of sync with its
+ * associated TabBar by delegating all mutations to the TabBar to this
+ * implementation of DeckPanel.
+ * </p>
+ */
+ private static class TabbedDeckPanel extends DeckPanel {
+ private UnmodifiableTabBar tabBar;
+
+ public TabbedDeckPanel(UnmodifiableTabBar tabBar) {
+ this.tabBar = tabBar;
+ }
+
+ public void clear() {
+ throw new UnsupportedOperationException(
+ "Use TabPanel.clear() to alter the DeckPanel");
+ }
+
+ public void insert(Widget w, int beforeIndex) {
+ throw new UnsupportedOperationException(
+ "Use TabPanel.insert() to alter the DeckPanel");
+ }
+
+ public boolean remove(Widget w) {
+ // Removal of items from the TabBar is delegated to the DeckPanel
+ // to ensure consistency
+ int idx = getWidgetIndex(w);
+ if (idx != -1) {
+ tabBar.removeTabProtected(idx);
+ return super.remove(w);
+ }
+
+ return false;
+ }
+
+ protected void insertProtected(Widget w, String tabText, boolean asHTML,
+ int beforeIndex) {
+
+ // 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(w);
+ if (idx != -1) {
+ remove(w);
+ if (idx < beforeIndex) {
+ beforeIndex--;
+ }
+ }
+
+ tabBar.insertTabProtected(tabText, asHTML, beforeIndex);
+ super.insert(w, beforeIndex);
+ }
+
+ protected void insertProtected(Widget w, Widget tabWidget,
+ int beforeIndex) {
+
+ // 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(w);
+ if (idx != -1) {
+ remove(w);
+ if (idx < beforeIndex) {
+ beforeIndex--;
+ }
+ }
+
+ tabBar.insertTabProtected(tabWidget, beforeIndex);
+ super.insert(w, beforeIndex);
+ }
+ };
+
+ /**
+ * This extension of TabPanel overrides the public mutator methods to
+ * prevent external callers from modifying the state of the TabBar.
+ */
+ private static class UnmodifiableTabBar extends TabBar {
+ public void insertTab(String text, boolean asHTML, int beforeIndex) {
+ throw new UnsupportedOperationException(
+ "Use TabPanel.insert() to alter the TabBar");
+ }
+
+ public void insertTab(Widget widget, int beforeIndex) {
+ throw new UnsupportedOperationException(
+ "Use TabPanel.insert() to alter the TabBar");
+ }
+
+ public void removeTab(int index) {
+ // It's possible for removeTab() to function correctly, but it's
+ // preferable to have only TabbedDeckPanel.remove() be operable,
+ // especially since TabBar does not export an Iterator over its values.
+ throw new UnsupportedOperationException(
+ "Use TabPanel.remove() to alter the TabBar");
+ }
+
+ protected void insertTabProtected(String text, boolean asHTML,
+ int beforeIndex) {
+ super.insertTab(text, asHTML, beforeIndex);
+ }
+
+ protected void insertTabProtected(Widget widget, int beforeIndex) {
+ super.insertTab(widget, beforeIndex);
+ }
+
+ protected void removeTabProtected(int index) {
+ super.removeTab(index);
+ }
+ }
+
+ private UnmodifiableTabBar tabBar = new UnmodifiableTabBar();
+ private TabbedDeckPanel deck = new TabbedDeckPanel(tabBar);
private TabListenerCollection tabListeners;
/**
@@ -76,7 +188,8 @@
}
/**
- * Adds a widget to the tab panel.
+ * Adds a widget to the tab panel. If the Widget is already attached to
+ * the TabPanel, it will be moved to the right-most index.
*
* @param w the widget to be added
* @param tabText the text to be shown on its tab
@@ -86,7 +199,8 @@
}
/**
- * Adds a widget to the tab panel.
+ * Adds a widget to the tab panel. If the Widget is already attached to
+ * the TabPanel, it will be moved to the right-most index.
*
* @param w the widget to be added
* @param tabText the text to be shown on its tab
@@ -97,7 +211,8 @@
}
/**
- * Adds a widget to the tab panel.
+ * Adds a widget to the tab panel. If the Widget is already attached to
+ * the TabPanel, it will be moved to the right-most index.
*
* @param w the widget to be added
* @param tabWidget the widget to be shown in the tab
@@ -120,7 +235,9 @@
}
/**
- * Gets the deck panel within this tab panel.
+ * Gets the deck panel within this tab panel. Adding or removing Widgets
+ * from the DeckPanel is not supported and will throw
+ * UnsupportedOperationExceptions.
*
* @return the deck panel
*/
@@ -129,7 +246,9 @@
}
/**
- * Gets the tab bar within this tab panel.
+ * Gets the tab bar within this tab panel. Adding or removing tabs from
+ * from the TabBar is not supported and will throw
+ * UnsupportedOperationExceptions.
*
* @return the tab bar
*/
@@ -138,19 +257,20 @@
}
public Widget getWidget(int index) {
- return children.get(index);
+ return deck.getWidget(index);
}
public int getWidgetCount() {
- return children.size();
+ return deck.getWidgetCount();
}
public int getWidgetIndex(Widget widget) {
- return children.indexOf(widget);
+ return deck.getWidgetIndex(widget);
}
/**
- * Inserts a widget into the tab panel.
+ * Inserts a widget into the tab panel. If the Widget is already attached
+ * to the TabPanel, it will be moved to the requested index.
*
* @param widget the widget to be inserted
* @param tabText the text to be shown on its tab
@@ -159,26 +279,26 @@
*/
public void insert(Widget widget, String tabText, boolean asHTML,
int beforeIndex) {
- children.insert(widget, beforeIndex);
- tabBar.insertTab(tabText, asHTML, beforeIndex);
- deck.insert(widget, beforeIndex);
+ // Delegate updates to the TabBar to our DeckPanel implementation
+ deck.insertProtected(widget, tabText, asHTML, beforeIndex);
}
/**
- * Inserts a widget into the tab panel.
+ * Inserts a widget into the tab panel. If the Widget is already attached
+ * to the TabPanel, it will be moved to the requested index.
*
* @param widget the widget to be inserted.
* @param tabWidget the widget to be shown on its tab.
* @param beforeIndex the index before which it will be inserted.
*/
public void insert(Widget widget, Widget tabWidget, int beforeIndex) {
- children.insert(widget, beforeIndex);
- tabBar.insertTab(tabWidget, beforeIndex);
- deck.insert(widget, beforeIndex);
+ // Delegate updates to the TabBar to our DeckPanel implementation
+ deck.insertProtected(widget, tabWidget, beforeIndex);
}
/**
- * Inserts a widget into the tab panel.
+ * Inserts a widget into the tab panel. If the Widget is already attached
+ * to the TabPanel, it will be moved to the requested index.
*
* @param widget the widget to be inserted
* @param tabText the text to be shown on its tab
@@ -189,7 +309,9 @@
}
public Iterator iterator() {
- return children.iterator();
+ // The Iterator returned by DeckPanel supports removal and will invoke
+ // TabbedDeckPanel.remove(), which is an active function.
+ return deck.iterator();
}
public boolean onBeforeTabSelected(SourcesTabEvents sender, int tabIndex) {
@@ -207,7 +329,8 @@
}
public boolean remove(int index) {
- return remove(getWidget(index));
+ // Delegate updates to the TabBar to our DeckPanel implementation
+ return deck.remove(index);
}
/**
@@ -216,15 +339,8 @@
* @param widget the widget to be removed
*/
public boolean remove(Widget widget) {
- int index = getWidgetIndex(widget);
- if (index == -1) {
- return false;
- }
-
- children.remove(widget);
- tabBar.removeTab(index);
- deck.remove(widget);
- return true;
+ // Delegate updates to the TabBar to our DeckPanel implementation
+ return deck.remove(widget);
}
public void removeTabListener(TabListener listener) {
diff --git a/user/test/com/google/gwt/user/client/ui/TabPanelTest.java b/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
index b54050d..5584b93 100644
--- a/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
@@ -27,6 +27,125 @@
public String getModuleName() {
return "com.google.gwt.user.User";
}
+
+ public void testUnmodifiableDeckPanelSubclasses() {
+ TabPanel p = new TabPanel();
+ DeckPanel d = p.getDeckPanel();
+
+ try {
+ d.add(new Label("No"));
+ fail("Internal DeckPanel should not allow add() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+
+ try {
+ d.insert(new Label("No"), 0);
+ fail("Internal DeckPanel should not allow insert() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+
+ try {
+ d.clear();
+ fail("Internal DeckPanel should not allow clear() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+ }
+
+ public void testUnmodifiableTabBarSubclasses() {
+ TabPanel p = new TabPanel();
+ TabBar b = p.getTabBar();
+
+ try {
+ b.addTab("no");
+ fail("Internal TabBar should not allow addTab() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+
+ try {
+ b.addTab("no", true);
+ fail("Internal TabBar should not allow addTab() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+
+ try {
+ b.addTab(new Label("no"));
+ fail("Internal TabBar should not allow addTab() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+
+ try {
+ b.insertTab("no", 0);
+ fail("Internal TabBar should not allow insertTab() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+
+ try {
+ b.insertTab("no", true, 0);
+ fail("Internal TabBar should not allow insertTab() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+
+ try {
+ b.insertTab(new Label("no"), 0);
+ fail("Internal TabBar should not allow insertTab() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+
+ try {
+ b.removeTab(0);
+ fail("Internal TabBar should not allow removeTab() method");
+ } catch (UnsupportedOperationException e) {
+ // Expected behavior
+ }
+ }
+
+ public void testInsertMultipleTimes() {
+ TabPanel p = new TabPanel();
+
+ 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 i = p.iterator();
+ assertTrue(i.hasNext());
+ assertTrue(tb.equals((Widget)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));
+ }
/**
* Tests to ensure that arbitrary widgets can be added/inserted effectively.