Adding a LoadingStateEvent to CellList and CellTable so users can receive an event when the data is loaded.  Previously, LoadingState was a package protected enum used to show the loading indicator in CellTable and empty list message in CellList.  Now, LoadingState is a first class citizen (an interface so users can define their own loading state).  Both CellList and CellTable now support setting a custom loading indicator Widget and a custom Widget to display when the table is empty.

Review at http://gwt-code-reviews.appspot.com/1338809

Review by: pdr@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9687 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java b/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
index 7275c1a..6c94271 100644
--- a/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
+++ b/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
@@ -20,6 +20,7 @@
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
@@ -28,7 +29,7 @@
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
-import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
+import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Focusable;
@@ -125,7 +126,7 @@
 
     public void setLoadingState(LoadingState state) {
       hasData.isRefreshing = true;
-      hasData.setLoadingState(state);
+      hasData.onLoadingStateChanged(state);
       hasData.isRefreshing = false;
     }
 
@@ -136,8 +137,7 @@
       // Use an anonymous class to override ValueChangeEvents's protected
       // constructor. We can't call ValueChangeEvent.fire() because this class
       // doesn't implement HasValueChangeHandlers.
-      hasData.fireEvent(new ValueChangeEvent<List<T>>(
-          hasData.getVisibleItems()) {
+      hasData.fireEvent(new ValueChangeEvent<List<T>>(hasData.getVisibleItems()) {
       });
     }
   }
@@ -279,8 +279,7 @@
     CellBasedWidgetImpl.get().sinkEvents(this, eventTypes);
 
     // Add a default selection event manager.
-    selectionManagerReg = addCellPreviewHandler(
-        DefaultSelectionEventManager.<T> createDefaultManager());
+    selectionManagerReg = addCellPreviewHandler(DefaultSelectionEventManager.<T> createDefaultManager());
   }
 
   public HandlerRegistration addCellPreviewHandler(
@@ -288,6 +287,18 @@
     return presenter.addCellPreviewHandler(handler);
   }
 
+  /**
+   * Add a {@link LoadingStateChangeEvent.Handler} to be notified of changes in
+   * the loading state.
+   * 
+   * @param handler the handle
+   * @return the registration for the handler
+   */
+  public HandlerRegistration addLoadingStateChangeHandler(
+      LoadingStateChangeEvent.Handler handler) {
+    return presenter.addLoadingStateChangeHandler(handler);
+  }
+
   public HandlerRegistration addRangeChangeHandler(
       RangeChangeEvent.Handler handler) {
     return presenter.addRangeChangeHandler(handler);
@@ -750,6 +761,16 @@
   protected void onFocus() {
   }
 
+  /**
+   * Called when the loading state changes. By default, this implementation
+   * fires a {@link LoadingStateChangeEvent}.
+   * 
+   * @param state the new loading state
+   */
+  protected void onLoadingStateChanged(LoadingState state) {
+    fireEvent(new LoadingStateChangeEvent(state));
+  }
+
   @Override
   protected void onUnload() {
     isFocused = false;
@@ -870,15 +891,51 @@
     return addHandler(handler, ValueChangeEvent.getType());
   }
 
+  /**
+   * Adopt the specified widget.
+   * 
+   * @param child the child to adopt
+   */
+  native void adopt(Widget child) /*-{
+    child.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(this);
+  }-*/;
+
+  /**
+   * Attach a child.
+   * 
+   * @param child the child to attach
+   */
+  native void doAttach(Widget child) /*-{
+    child.@com.google.gwt.user.client.ui.Widget::onAttach()();
+  }-*/;
+
+  /**
+   * Detach a child.
+   * 
+   * @param child the child to detach
+   */
+  native void doDetach(Widget child) /*-{
+    child.@com.google.gwt.user.client.ui.Widget::onDetach()();
+  }-*/;
+
   HasDataPresenter<T> getPresenter() {
     return presenter;
   }
 
   /**
-   * Set the current loading state of the data.
-   *
-   * @param state the loading state
+   * Show or hide an element.
+   * 
+   * @param element the element
+   * @param show true to show, false to hide
    */
-  void setLoadingState(LoadingState state) {
+  void showOrHide(Element element, boolean show) {
+    if (element == null) {
+      return;
+    }
+    if (show) {
+      element.getStyle().clearDisplay();
+    } else {
+      element.getStyle().setDisplay(Display.NONE);
+    }
   }
 }
diff --git a/user/src/com/google/gwt/user/cellview/client/CellList.java b/user/src/com/google/gwt/user/cellview/client/CellList.java
index c7100d1..7a85a9d 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellList.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellList.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 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
@@ -24,7 +24,6 @@
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.EventTarget;
-import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
@@ -35,18 +34,24 @@
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
-import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
+import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
 import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.AttachDetachException;
+import com.google.gwt.user.client.ui.DeckPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.view.client.CellPreviewEvent;
 import com.google.gwt.view.client.ProvidesKey;
 import com.google.gwt.view.client.SelectionModel;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
 /**
  * A single column list of cells.
- *
+ * 
  * <p>
  * <h3>Examples</h3>
  * <dl>
@@ -58,7 +63,7 @@
  * <dd>{@example com.google.gwt.examples.view.KeyProviderExample}</dd>
  * </dl>
  * </p>
- *
+ * 
  * @param <T> the data type of list items
  */
 public class CellList<T> extends AbstractHasData<T> {
@@ -148,9 +153,14 @@
   private final Cell<T> cell;
   private boolean cellIsEditing;
   private final Element childContainer;
-
   private SafeHtml emptyListMessage = SafeHtmlUtils.fromSafeConstant("");
-  private final Element emptyMessageElem;
+  private final SimplePanel emptyListWidgetContainer = new SimplePanel();
+  private final SimplePanel loadingIndicatorContainer = new SimplePanel();
+
+  /**
+   * A {@link DeckPanel} to hold widgets associated with various loading states.
+   */
+  private final DeckPanel messagesPanel = new DeckPanel();
 
   private final Style style;
 
@@ -158,7 +168,7 @@
 
   /**
    * Construct a new {@link CellList}.
-   *
+   * 
    * @param cell the cell used to render each item
    */
   public CellList(final Cell<T> cell) {
@@ -167,7 +177,7 @@
 
   /**
    * Construct a new {@link CellList} with the specified {@link Resources}.
-   *
+   * 
    * @param cell the cell used to render each item
    * @param resources the resources used for this widget
    */
@@ -176,26 +186,28 @@
   }
 
   /**
-   * Construct a new {@link CellList} with the specified {@link ProvidesKey key provider}.
-   *
+   * Construct a new {@link CellList} with the specified {@link ProvidesKey key
+   * provider}.
+   * 
    * @param cell the cell used to render each item
    * @param keyProvider an instance of ProvidesKey<T>, or null if the record
-   *        object should act as its own key
+   *          object should act as its own key
    */
   public CellList(final Cell<T> cell, ProvidesKey<T> keyProvider) {
     this(cell, getDefaultResources(), keyProvider);
   }
 
   /**
-   * Construct a new {@link CellList} with the specified {@link Resources}
-   * and {@link ProvidesKey key provider}.
-   *
+   * Construct a new {@link CellList} with the specified {@link Resources} and
+   * {@link ProvidesKey key provider}.
+   * 
    * @param cell the cell used to render each item
    * @param resources the resources used for this widget
    * @param keyProvider an instance of ProvidesKey<T>, or null if the record
-   *        object should act as its own key
+   *          object should act as its own key
    */
-  public CellList(final Cell<T> cell, Resources resources, ProvidesKey<T> keyProvider) {
+  public CellList(final Cell<T> cell, Resources resources,
+      ProvidesKey<T> keyProvider) {
     super(Document.get().createDivElement(), DEFAULT_PAGE_SIZE, keyProvider);
     this.cell = cell;
     this.style = resources.cellListStyle();
@@ -207,15 +219,16 @@
       addStyleName(widgetStyle);
     }
 
-    // Create the DOM hierarchy.
+    // Add the child container.
     childContainer = Document.get().createDivElement();
-
-    emptyMessageElem = Document.get().createDivElement();
-    showOrHide(emptyMessageElem, false);
-
     DivElement outerDiv = getElement().cast();
     outerDiv.appendChild(childContainer);
-    outerDiv.appendChild(emptyMessageElem);
+
+    // Attach the message panel.
+    outerDiv.appendChild(messagesPanel.getElement());
+    adopt(messagesPanel);
+    messagesPanel.add(emptyListWidgetContainer);
+    messagesPanel.add(loadingIndicatorContainer);
 
     // Sink events that the cell consumes.
     CellBasedWidgetImpl.get().sinkEvents(this, cell.getConsumedEvents());
@@ -223,18 +236,38 @@
 
   /**
    * Get the message that is displayed when there is no data.
-   *
+   * 
    * @return the empty message
    * @see #setEmptyListMessage(SafeHtml)
+   * @deprecated as of GWT 2.3, use {@link #getEmptyListWidget()} instead
    */
+  @Deprecated
   public SafeHtml getEmptyListMessage() {
     return emptyListMessage;
   }
 
   /**
+   * Get the widget displayed when the list has no rows.
+   * 
+   * @return the empty list widget
+   */
+  public Widget getEmptyListWidget() {
+    return emptyListWidgetContainer.getWidget();
+  }
+
+  /**
+   * Get the widget displayed when the data is loading.
+   * 
+   * @return the loading indicator
+   */
+  public Widget getLoadingIndicator() {
+    return loadingIndicatorContainer.getWidget();
+  }
+
+  /**
    * Get the {@link Element} for the specified index. If the element has not
    * been created, null is returned.
-   *
+   * 
    * @param indexOnPage the index on the page
    * @return the element, or null if it doesn't exists
    * @throws IndexOutOfBoundsException if the index is outside of the current
@@ -251,18 +284,40 @@
 
   /**
    * Set the message to display when there is no data.
-   *
+   * 
    * @param html the message to display when there are no results
    * @see #getEmptyListMessage()
+   * @deprecated as of GWT 2.3, use
+   *             {@link #setEmptyDataWidget(com.google.gwt.user.client.ui.Widget)}
+   *             instead
    */
+  @Deprecated
   public void setEmptyListMessage(SafeHtml html) {
     this.emptyListMessage = html;
-    emptyMessageElem.setInnerHTML(html.asString());
+    setEmptyListWidget(html == null ? null : new HTML(html));
+  }
+
+  /**
+   * Set the widget to display when the list has no rows.
+   * 
+   * @param widget the empty data widget
+   */
+  public void setEmptyListWidget(Widget widget) {
+    emptyListWidgetContainer.setWidget(widget);
+  }
+
+  /**
+   * Set the widget to display when the data is loading.
+   * 
+   * @param widget the loading indicator
+   */
+  public void setLoadingIndicator(Widget widget) {
+    loadingIndicatorContainer.setWidget(widget);
   }
 
   /**
    * Set the value updater to use when cells modify items.
-   *
+   * 
    * @param valueUpdater the {@link ValueUpdater}
    */
   public void setValueUpdater(ValueUpdater<T> valueUpdater) {
@@ -274,6 +329,24 @@
     return cell.dependsOnSelection();
   }
 
+  @Override
+  protected void doAttachChildren() {
+    try {
+      doAttach(messagesPanel);
+    } catch (Throwable e) {
+      throw new AttachDetachException(Collections.singleton(e));
+    }
+  }
+
+  @Override
+  protected void doDetachChildren() {
+    try {
+      doDetach(messagesPanel);
+    } catch (Throwable e) {
+      throw new AttachDetachException(Collections.singleton(e));
+    }
+  }
+
   /**
    * Called when a user action triggers selection.
    * 
@@ -323,7 +396,7 @@
   /**
    * Get the parent element that wraps the cell from the list item. Override
    * this method if you add structure to the element.
-   *
+   * 
    * @param item the row element that wraps the list item
    * @return the parent element of the cell
    */
@@ -429,6 +502,35 @@
     }
   }
 
+  /**
+   * Called when the loading state changes.
+   * 
+   * @param state the new loading state
+   */
+  @Override
+  protected void onLoadingStateChanged(LoadingState state) {
+    Widget message = null;
+    if (state == LoadingState.LOADING) {
+      // Loading indicator.
+      message = loadingIndicatorContainer;
+    } else if (state == LoadingState.LOADED && getPresenter().isEmpty()) {
+      // Empty table.
+      message = emptyListWidgetContainer;
+    }
+
+    // Switch out the message to display.
+    if (message != null) {
+      messagesPanel.showWidget(messagesPanel.getWidgetIndex(message));
+    }
+
+    // Show the correct container.
+    showOrHide(getChildContainer(), message == null);
+    messagesPanel.setVisible(message != null);
+
+    // Fire an event.
+    super.onLoadingStateChanged(state);
+  }
+
   @Override
   protected void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
       SelectionModel<? super T> selectionModel) {
@@ -515,24 +617,4 @@
   protected void setSelected(Element elem, boolean selected) {
     setStyleName(elem, style.cellListSelectedItem(), selected);
   }
-
-  @Override
-  void setLoadingState(LoadingState state) {
-    showOrHide(emptyMessageElem, state == LoadingState.EMPTY);
-    // TODO(jlabanca): Add a loading icon.
-  }
-
-  /**
-   * Show or hide an element.
-   *
-   * @param element the element
-   * @param show true to show, false to hide
-   */
-  private void showOrHide(Element element, boolean show) {
-    if (show) {
-      element.getStyle().clearDisplay();
-    } else {
-      element.getStyle().setDisplay(Display.NONE);
-    }
-  }
 }
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTable.css b/user/src/com/google/gwt/user/cellview/client/CellTable.css
index 6a73c90..061aba8 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTable.css
+++ b/user/src/com/google/gwt/user/cellview/client/CellTable.css
@@ -132,7 +132,6 @@
   border: selectionBorderWidth solid #d7dde8;
 }
 
-@sprite .cellTableLoading {
-  gwt-image: 'cellTableLoading';
+.cellTableLoading {
   margin: 30px;
 }
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTable.java b/user/src/com/google/gwt/user/cellview/client/CellTable.java
index 4da9d35..c01d333 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTable.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTable.java
@@ -25,7 +25,6 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.EventTarget;
 import com.google.gwt.dom.client.NodeList;
-import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.dom.client.Style.TableLayout;
 import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.dom.client.TableCellElement;
@@ -47,11 +46,15 @@
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
 import com.google.gwt.user.cellview.client.ColumnSortList.ColumnSortInfo;
-import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
+import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.DeckPanel;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment.HorizontalAlignmentConstant;
 import com.google.gwt.user.client.ui.HasVerticalAlignment.VerticalAlignmentConstant;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.view.client.CellPreviewEvent;
 import com.google.gwt.view.client.ProvidesKey;
 import com.google.gwt.view.client.SelectionModel;
@@ -317,9 +320,6 @@
     @Template("<div style=\"outline:none;\" tabindex=\"{0}\" accessKey=\"{1}\">{2}</div>")
     SafeHtml divFocusableWithKey(int tabIndex, char accessKey, SafeHtml contents);
 
-    @Template("<div class=\"{0}\"></div>")
-    SafeHtml loading(String loading);
-
     @Template("<table><tbody>{0}</tbody></table>")
     SafeHtml tbody(SafeHtml rowHtml);
 
@@ -493,6 +493,7 @@
    */
   private boolean dependsOnSelection;
 
+  private final SimplePanel emptyTableWidgetContainer = new SimplePanel();
   private final List<Header<?>> footers = new ArrayList<Header<?>>();
 
   /**
@@ -510,6 +511,12 @@
   private boolean isInteractive;
 
   private int keyboardSelectedColumn = 0;
+  private final SimplePanel loadingIndicatorContainer = new SimplePanel();
+
+  /**
+   * A {@link DeckPanel} to hold widgets associated with various loading states.
+   */
+  private final DeckPanel messagesPanel = new DeckPanel();
 
   private final Resources resources;
   private RowStyles<T> rowStyles;
@@ -520,6 +527,7 @@
   private final TableElement table;
   private final TableSectionElement tbody;
   private final TableSectionElement tbodyLoading;
+  private final TableCellElement tbodyLoadingCell;
   private final TableSectionElement tfoot;
   private final TableSectionElement thead;
   private boolean updatingSortList;
@@ -621,17 +629,23 @@
     tfoot = table.createTFoot();
     setStyleName(this.style.cellTableWidget());
 
-    // Create the loading indicator.
+    // Attach the messages panel.
     {
-      TableCellElement td = Document.get().createTDElement();
+      tbodyLoadingCell = Document.get().createTDElement();
       TableRowElement tr = Document.get().createTRElement();
       tbodyLoading.appendChild(tr);
-      tr.appendChild(td);
-      td.setAlign("center");
-      td.setInnerHTML(template.loading(style.cellTableLoading()).asString());
-      setLoadingIconVisible(false);
+      tr.appendChild(tbodyLoadingCell);
+      tbodyLoadingCell.setAlign("center");
+      tbodyLoadingCell.appendChild(messagesPanel.getElement());
+      adopt(messagesPanel);
+      messagesPanel.add(emptyTableWidgetContainer);
+      messagesPanel.add(loadingIndicatorContainer);
+      loadingIndicatorContainer.setStyleName(style.cellTableLoading());
     }
 
+    // Set the default loading indicator.
+    setLoadingIndicator(new Image(resources.cellTableLoading()));
+
     // Sink events.
     Set<String> eventTypes = new HashSet<String>();
     eventTypes.add("mouseover");
@@ -800,6 +814,15 @@
   }
 
   /**
+   * Get the widget displayed when the table has no rows.
+   * 
+   * @return the empty table widget
+   */
+  public Widget getEmptyTableWidget() {
+    return emptyTableWidgetContainer.getWidget();
+  }
+
+  /**
    * Return the height of the table header.
    * 
    * @return an int representing the header height
@@ -810,6 +833,15 @@
   }
 
   /**
+   * Get the widget displayed when the data is loading.
+   * 
+   * @return the loading indicator
+   */
+  public Widget getLoadingIndicator() {
+    return loadingIndicatorContainer.getWidget();
+  }
+
+  /**
    * Get the {@link TableRowElement} for the specified row. If the row element
    * has not been created, null is returned.
    * 
@@ -1066,6 +1098,24 @@
   }
 
   /**
+   * Set the widget to display when the table has no rows.
+   * 
+   * @param widget the empty table widget
+   */
+  public void setEmptyTableWidget(Widget widget) {
+    emptyTableWidgetContainer.setWidget(widget);
+  }
+
+  /**
+   * Set the widget to display when the data is loading.
+   * 
+   * @param widget the loading indicator
+   */
+  public void setLoadingIndicator(Widget widget) {
+    loadingIndicatorContainer.setWidget(widget);
+  }
+
+  /**
    * Sets the object used to determine how a row is styled; the change will take
    * effect the next time that the table is rendered.
    * 
@@ -1322,6 +1372,38 @@
     }
   }
 
+  /**
+   * Called when the loading state changes.
+   * 
+   * @param state the new loading state
+   */
+  @Override
+  protected void onLoadingStateChanged(LoadingState state) {
+    Widget message = null;
+    if (state == LoadingState.LOADING) {
+      // Loading indicator.
+      message = loadingIndicatorContainer;
+    } else if (state == LoadingState.LOADED && getPresenter().isEmpty()) {
+      // Empty table.
+      message = emptyTableWidgetContainer;
+    }
+
+    // Switch out the message to display.
+    if (message != null) {
+      messagesPanel.showWidget(messagesPanel.getWidgetIndex(message));
+    }
+
+    // Adjust the colspan of the messages panel container.
+    tbodyLoadingCell.setColSpan(Math.max(1, columns.size()));
+
+    // Show the correct container.
+    showOrHide(getChildContainer(), message == null);
+    showOrHide(tbodyLoading, message != null);
+
+    // Fire an event.
+    super.onLoadingStateChanged(state);
+  }
+
   @Override
   protected void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
       SelectionModel<? super T> selectionModel) {
@@ -1501,11 +1583,6 @@
         style.cellTableSelectedRowCell(), selected);
   }
 
-  @Override
-  void setLoadingState(LoadingState state) {
-    setLoadingIconVisible(state == LoadingState.LOADING);
-  }
-
   /**
    * Check that the specified column is within bounds.
    * 
@@ -1889,26 +1966,6 @@
   }
 
   /**
-   * Show or hide the loading icon.
-   * 
-   * @param visible true to show, false to hide.
-   */
-  private void setLoadingIconVisible(boolean visible) {
-    // Clear the current data.
-    if (visible) {
-      tbody.getStyle().setDisplay(Display.NONE);
-    } else {
-      tbody.getStyle().clearDisplay();
-    }
-
-    // Update the colspan.
-    TableCellElement td = tbodyLoading.getRows().getItem(0).getCells().getItem(
-        0);
-    td.setColSpan(Math.max(1, columns.size()));
-    setVisible(tbodyLoading, visible);
-  }
-
-  /**
    * Apply a style to a row and all cells in the row.
    * 
    * @param tr the row element
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTableBasic.css b/user/src/com/google/gwt/user/cellview/client/CellTableBasic.css
index c34771c..8f98e26 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTableBasic.css
+++ b/user/src/com/google/gwt/user/cellview/client/CellTableBasic.css
@@ -137,7 +137,6 @@
   background: #d7dde8;
 }
 
-@sprite .cellTableLoading {
-  gwt-image: 'cellTableLoading';
+.cellTableLoading {
   margin: 30px;
 }
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
index 22c0938..ae77b7c 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
@@ -37,8 +37,8 @@
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
-import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
 import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.impl.FocusImpl;
 import com.google.gwt.view.client.CellPreviewEvent;
@@ -252,7 +252,8 @@
 
       public void setLoadingState(LoadingState state) {
         nodeView.updateImage(state == LoadingState.LOADING);
-        showOrHide(nodeView.emptyMessageElem, state == LoadingState.EMPTY);
+        showOrHide(nodeView.emptyMessageElem, state == LoadingState.LOADED
+            && presenter.isEmpty());
       }
 
       /**
diff --git a/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java b/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
index ee3789c..b514593 100644
--- a/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
+++ b/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
@@ -23,7 +23,7 @@
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
-import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
 import com.google.gwt.view.client.CellPreviewEvent;
 import com.google.gwt.view.client.HasData;
 import com.google.gwt.view.client.HasKeyProvider;
@@ -83,16 +83,6 @@
   }
 
   /**
-   * The loading state of the data.
-   */
-  static enum LoadingState {
-    LOADING, // Waiting for data to load.
-    PARTIALLY_LOADED, // Partial page data loaded.
-    LOADED, // All page data loaded.
-    EMPTY; // The data size is 0.
-  }
-
-  /**
    * The view that this presenter presents.
    * 
    * @param <T> the data type
@@ -474,6 +464,11 @@
     return view.addHandler(handler, CellPreviewEvent.getType());
   }
 
+  public HandlerRegistration addLoadingStateChangeHandler(
+      LoadingStateChangeEvent.Handler handler) {
+    return view.addHandler(handler, LoadingStateChangeEvent.TYPE);
+  }
+
   public HandlerRegistration addRangeChangeHandler(
       RangeChangeEvent.Handler handler) {
     return view.addHandler(handler, RangeChangeEvent.getType());
@@ -657,6 +652,16 @@
     return pendingState != null;
   }
 
+  /**
+   * Check whether or not the data set is empty. That is, the row count is
+   * exactly 0.
+   * 
+   * @return true if data set is empty
+   */
+  public boolean isEmpty() {
+    return isRowCountExact() && getRowCount() == 0;
+  }
+
   public boolean isRowCountExact() {
     return getCurrentState().isRowCountExact();
   }
@@ -1512,9 +1517,7 @@
   private void updateLoadingState() {
     int cacheSize = getVisibleItemCount();
     int curPageSize = isRowCountExact() ? getCurrentPageSize() : getPageSize();
-    if (getRowCount() == 0 && isRowCountExact()) {
-      view.setLoadingState(LoadingState.EMPTY);
-    } else if (cacheSize >= curPageSize) {
+    if (cacheSize >= curPageSize) {
       view.setLoadingState(LoadingState.LOADED);
     } else if (cacheSize == 0) {
       view.setLoadingState(LoadingState.LOADING);
diff --git a/user/src/com/google/gwt/user/cellview/client/LoadingStateChangeEvent.java b/user/src/com/google/gwt/user/cellview/client/LoadingStateChangeEvent.java
new file mode 100644
index 0000000..16fc719
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/LoadingStateChangeEvent.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2011 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.cellview.client;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * An event used to indicate that the data loading state has changed.
+ */
+public class LoadingStateChangeEvent extends
+    GwtEvent<LoadingStateChangeEvent.Handler> {
+
+  /**
+   * Implemented by handlers of {@link LoadingStateChangeEvent}.
+   */
+  public interface Handler extends EventHandler {
+    /**
+     * Called when a {@link LoadingStateChangeEvent} is fired.
+     * 
+     * @param event the {@link LoadingStateChangeEvent}
+     */
+    void onLoadingStateChanged(LoadingStateChangeEvent event);
+  }
+
+  /**
+   * Represents the current status of the data being loaded.
+   */
+  public static interface LoadingState {
+    /**
+     * Indicates that the data has started to load.
+     */
+    LoadingState LOADING = new DefaultLoadingState();
+
+    /**
+     * Indicates that part of the data set has been loaded, but more data is
+     * still pending.
+     */
+    LoadingState PARTIALLY_LOADED = new DefaultLoadingState();
+
+    /**
+     * Indicates that the data set has been completely loaded.
+     */
+    LoadingState LOADED = new DefaultLoadingState();
+  }
+
+  /**
+   * Default implementation of {@link LoadingState}.
+   */
+  private static class DefaultLoadingState implements LoadingState {
+  }
+
+  /**
+   * A singleton instance of Type.
+   */
+  public static final Type<Handler> TYPE = new Type<Handler>();
+
+  private final LoadingState state;
+
+  /**
+   * Construct a new {@link LoadingStateChangeEvent}.
+   * 
+   * @param state the new state
+   */
+  public LoadingStateChangeEvent(LoadingState state) {
+    this.state = state;
+  }
+
+  @Override
+  public Type<Handler> getAssociatedType() {
+    return TYPE;
+  }
+
+  /**
+   * Get the new {@link LoadingState} associated with this event.
+   * 
+   * @return the {@link LoadingState}
+   */
+  public LoadingState getLoadingState() {
+    return state;
+  }
+
+  @Override
+  protected void dispatch(Handler handler) {
+    handler.onLoadingStateChanged(this);
+  }
+}
diff --git a/user/test/com/google/gwt/user/cellview/client/CellListTest.java b/user/test/com/google/gwt/user/cellview/client/CellListTest.java
index cc42e22..98b5b9e 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellListTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellListTest.java
@@ -21,6 +21,8 @@
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwt.view.client.ProvidesKey;
 
@@ -89,6 +91,39 @@
     assertEquals(10, rendered.size());
   }
 
+  @SuppressWarnings("deprecation")
+  public void testSetEmptyListWidget() {
+    CellList<String> cellList = createAbstractHasData(new TextCell());
+
+    // Set a widget.
+    Label l = new Label("Empty");
+    cellList.setEmptyListWidget(l);
+    assertEquals(l, cellList.getEmptyListWidget());
+
+    // Set a message.
+    SafeHtml message = SafeHtmlUtils.fromString("empty");
+    cellList.setEmptyListMessage(message);
+    assertEquals(message, cellList.getEmptyListMessage());
+    assertNotSame(l, cellList.getEmptyListWidget());
+
+    // Null widget.
+    cellList.setEmptyListWidget(null);
+    assertNull(cellList.getEmptyListWidget());
+  }
+
+  public void testSetLoadingIndicator() {
+    CellList<String> cellList = createAbstractHasData(new TextCell());
+
+    // Set a widget.
+    Label l = new Label("Loading");
+    cellList.setLoadingIndicator(l);
+    assertEquals(l, cellList.getLoadingIndicator());
+
+    // Null widget.
+    cellList.setLoadingIndicator(null);
+    assertNull(cellList.getLoadingIndicator());
+  }
+
   @Override
   protected CellList<String> createAbstractHasData(Cell<String> cell) {
     return new CellList<String>(cell);
diff --git a/user/test/com/google/gwt/user/cellview/client/CellTableTest.java b/user/test/com/google/gwt/user/cellview/client/CellTableTest.java
index 9147ffd..6c27ac2 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellTableTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellTableTest.java
@@ -33,6 +33,7 @@
 import com.google.gwt.user.cellview.client.CellTable.Style;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
 import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.RootPanel;
 
 import java.util.ArrayList;
@@ -424,6 +425,32 @@
     assertEquals("", col1.getStyle().getWidth());
   }
 
+  public void testSetEmptyListWidget() {
+    CellTable<String> table = createAbstractHasData(new TextCell());
+
+    // Set a widget.
+    Label l = new Label("Empty");
+    table.setEmptyTableWidget(l);
+    assertEquals(l, table.getEmptyTableWidget());
+
+    // Null widget.
+    table.setEmptyTableWidget(null);
+    assertNull(table.getEmptyTableWidget());
+  }
+
+  public void testSetLoadingIndicator() {
+    CellTable<String> table = createAbstractHasData(new TextCell());
+
+    // Set a widget.
+    Label l = new Label("Loading");
+    table.setLoadingIndicator(l);
+    assertEquals(l, table.getLoadingIndicator());
+
+    // Null widget.
+    table.setLoadingIndicator(null);
+    assertNull(table.getLoadingIndicator());
+  }
+
   public void testSetTableLayoutFixed() {
     CellTable<String> table = createAbstractHasData(new TextCell());
     assertNotSame("fixed",
diff --git a/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java b/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
index 1ff0fa6..358a5c9 100644
--- a/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
@@ -23,10 +23,10 @@
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
-import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
 import com.google.gwt.user.cellview.client.HasDataPresenter.View;
 import com.google.gwt.user.cellview.client.HasKeyboardPagingPolicy.KeyboardPagingPolicy;
 import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
 import com.google.gwt.view.client.HasData;
 import com.google.gwt.view.client.MockHasData;
 import com.google.gwt.view.client.MockHasData.MockRangeChangeHandler;
@@ -497,6 +497,31 @@
     assertEquals(5, presenter.getCurrentPageSize());
   }
 
+  public void testIsEmpty() {
+    HasData<String> listView = new MockHasData<String>();
+    MockView<String> view = new MockView<String>();
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
+        view, 10, null);
+
+    // Non-zero row count.
+    presenter.setRowCount(1, true);
+    populatePresenter(presenter);
+    presenter.flush();
+    assertFalse(presenter.isEmpty());
+
+    // Zero row count with unknown size.
+    presenter.setRowCount(0, false);
+    populatePresenter(presenter);
+    presenter.flush();
+    assertFalse(presenter.isEmpty());
+
+    // Zero row count with known size.
+    presenter.setRowCount(0, true);
+    populatePresenter(presenter);
+    presenter.flush();
+    assertFalse(presenter.isEmpty());
+  }
+  
   public void testKeyboardNavigationChangePage() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
@@ -1246,7 +1271,7 @@
     assertEquals(0, presenter.getRowCount());
     assertTrue(presenter.isRowCountExact());
     presenter.flush();
-    view.assertLoadingState(LoadingState.EMPTY);
+    view.assertLoadingState(LoadingState.LOADED);
   }
 
   public void testSetRowCountNoBoolean() {