Adding configuration booleans to CellTable to refresh headers and footers only when needed.  Currently, the headers and footers are refreshed every time the data is redrawn, even though headers and footers are often static or do not depend on the data.  Users can now call #setAutoHeader/FooterRefreshDisabled() to disable this feature.  Headers and footers will still be redrawn when a column is inserted or removed, but they will not be redrawn on every data update.  Users can force the headers/footers to redraw synchronously by calling #redrawHeaders/Footers.

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

Review by: pengzhuang@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10522 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCellTable.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCellTable.java
index 79a7d16..153b0c4 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCellTable.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCellTable.java
@@ -130,6 +130,10 @@
         ContactDatabase.ContactInfo.KEY_PROVIDER);
     cellTable.setWidth("100%", true);
 
+    // Do not refresh the headers and footers every time the data is updated.
+    cellTable.setAutoHeaderRefreshDisabled(true);
+    cellTable.setAutoFooterRefreshDisabled(true);
+
     // Attach a column sort handler to the ListDataProvider to sort the list.
     ListHandler<ContactInfo> sortHandler = new ListHandler<ContactInfo>(
         ContactDatabase.get().getDataProvider().getList());
@@ -163,10 +167,12 @@
   protected void asyncOnInitialize(final AsyncCallback<Widget> callback) {
     GWT.runAsync(CwCellTable.class, new RunAsyncCallback() {
 
+      @Override
       public void onFailure(Throwable caught) {
         callback.onFailure(caught);
       }
 
+      @Override
       public void onSuccess() {
         callback.onSuccess(onInitialize());
       }
@@ -204,12 +210,14 @@
     };
     firstNameColumn.setSortable(true);
     sortHandler.setComparator(firstNameColumn, new Comparator<ContactInfo>() {
+      @Override
       public int compare(ContactInfo o1, ContactInfo o2) {
         return o1.getFirstName().compareTo(o2.getFirstName());
       }
     });
     cellTable.addColumn(firstNameColumn, constants.cwCellTableColumnFirstName());
     firstNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+      @Override
       public void update(int index, ContactInfo object, String value) {
         // Called when the user changes the value.
         object.setFirstName(value);
@@ -228,12 +236,14 @@
     };
     lastNameColumn.setSortable(true);
     sortHandler.setComparator(lastNameColumn, new Comparator<ContactInfo>() {
+      @Override
       public int compare(ContactInfo o1, ContactInfo o2) {
         return o1.getLastName().compareTo(o2.getLastName());
       }
     });
     cellTable.addColumn(lastNameColumn, constants.cwCellTableColumnLastName());
     lastNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+      @Override
       public void update(int index, ContactInfo object, String value) {
         // Called when the user changes the value.
         object.setLastName(value);
@@ -258,6 +268,7 @@
     };
     cellTable.addColumn(categoryColumn, constants.cwCellTableColumnCategory());
     categoryColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+      @Override
       public void update(int index, ContactInfo object, String value) {
         for (Category category : categories) {
           if (category.getDisplayName().equals(value)) {
@@ -280,6 +291,7 @@
     addressColumn.setSortable(true);
     addressColumn.setDefaultSortAscending(false);
     sortHandler.setComparator(addressColumn, new Comparator<ContactInfo>() {
+      @Override
       public int compare(ContactInfo o1, ContactInfo o2) {
         return o1.getAddress().compareTo(o2.getAddress());
       }
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.java
index 568af54..2386841 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.java
@@ -549,12 +549,21 @@
 
     // Create a DataGrid.
 
-    // Set a key provider that provides a unique key for each contact. If key is
-    // used to identify contacts when fields (such as the name and address)
-    // change.
+    /*
+     * Set a key provider that provides a unique key for each contact. If key is
+     * used to identify contacts when fields (such as the name and address)
+     * change.
+     */
     dataGrid = new DataGrid<ContactInfo>(ContactDatabase.ContactInfo.KEY_PROVIDER);
     dataGrid.setWidth("100%");
 
+    /*
+     * Do not refresh the headers every time the data is updated. The footer
+     * depends on the current data, so we do not disable auto refresh on the
+     * footer.
+     */
+    dataGrid.setAutoHeaderRefreshDisabled(true);
+
     // Set the message to display when the table is empty.
     dataGrid.setEmptyTableWidget(new Label(constants.cwCustomDataGridEmpty()));
 
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwDataGrid.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwDataGrid.java
index 0238b85..7e6149a 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwDataGrid.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwDataGrid.java
@@ -136,12 +136,21 @@
   public Widget onInitialize() {
     // Create a DataGrid.
 
-    // Set a key provider that provides a unique key for each contact. If key is
-    // used to identify contacts when fields (such as the name and address)
-    // change.
+    /*
+     * Set a key provider that provides a unique key for each contact. If key is
+     * used to identify contacts when fields (such as the name and address)
+     * change.
+     */
     dataGrid = new DataGrid<ContactInfo>(ContactDatabase.ContactInfo.KEY_PROVIDER);
     dataGrid.setWidth("100%");
 
+    /*
+     * Do not refresh the headers every time the data is updated. The footer
+     * depends on the current data, so we do not disable auto refresh on the
+     * footer.
+     */
+    dataGrid.setAutoHeaderRefreshDisabled(true);
+
     // Set the message to display when the table is empty.
     dataGrid.setEmptyTableWidget(new Label(constants.cwDataGridEmpty()));
 
@@ -176,10 +185,12 @@
   protected void asyncOnInitialize(final AsyncCallback<Widget> callback) {
     GWT.runAsync(CwDataGrid.class, new RunAsyncCallback() {
 
+      @Override
       public void onFailure(Throwable caught) {
         callback.onFailure(caught);
       }
 
+      @Override
       public void onSuccess() {
         callback.onSuccess(onInitialize());
       }
@@ -216,12 +227,14 @@
         };
     firstNameColumn.setSortable(true);
     sortHandler.setComparator(firstNameColumn, new Comparator<ContactInfo>() {
+      @Override
       public int compare(ContactInfo o1, ContactInfo o2) {
         return o1.getFirstName().compareTo(o2.getFirstName());
       }
     });
     dataGrid.addColumn(firstNameColumn, constants.cwDataGridColumnFirstName());
     firstNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+      @Override
       public void update(int index, ContactInfo object, String value) {
         // Called when the user changes the value.
         object.setFirstName(value);
@@ -240,12 +253,14 @@
         };
     lastNameColumn.setSortable(true);
     sortHandler.setComparator(lastNameColumn, new Comparator<ContactInfo>() {
+      @Override
       public int compare(ContactInfo o1, ContactInfo o2) {
         return o1.getLastName().compareTo(o2.getLastName());
       }
     });
     dataGrid.addColumn(lastNameColumn, constants.cwDataGridColumnLastName());
     lastNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+      @Override
       public void update(int index, ContactInfo object, String value) {
         // Called when the user changes the value.
         object.setLastName(value);
@@ -263,6 +278,7 @@
     };
     lastNameColumn.setSortable(true);
     sortHandler.setComparator(ageColumn, new Comparator<ContactInfo>() {
+      @Override
       public int compare(ContactInfo o1, ContactInfo o2) {
         return o1.getBirthday().compareTo(o2.getBirthday());
       }
@@ -301,6 +317,7 @@
     };
     dataGrid.addColumn(categoryColumn, constants.cwDataGridColumnCategory());
     categoryColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+      @Override
       public void update(int index, ContactInfo object, String value) {
         for (Category category : categories) {
           if (category.getDisplayName().equals(value)) {
@@ -321,6 +338,7 @@
     };
     addressColumn.setSortable(true);
     sortHandler.setComparator(addressColumn, new Comparator<ContactInfo>() {
+      @Override
       public int compare(ContactInfo o1, ContactInfo o2) {
         return o1.getAddress().compareTo(o2.getAddress());
       }
diff --git a/user/src/com/google/gwt/user/cellview/client/AbstractCellTable.java b/user/src/com/google/gwt/user/cellview/client/AbstractCellTable.java
index 8287184..0cd4dbc 100644
--- a/user/src/com/google/gwt/user/cellview/client/AbstractCellTable.java
+++ b/user/src/com/google/gwt/user/cellview/client/AbstractCellTable.java
@@ -961,6 +961,7 @@
   private boolean dependsOnSelection;
 
   private Widget emptyTableWidget;
+  private boolean footerRefreshDisabled;
   private final List<Header<?>> footers = new ArrayList<Header<?>>();
 
   /**
@@ -968,8 +969,15 @@
    */
   private boolean handlesSelection;
 
+  private boolean headerRefreshDisabled;
   private final List<Header<?>> headers = new ArrayList<Header<?>>();
 
+  /**
+   * Indicates that either the headers or footers are dirty, and both should be
+   * refreshed the next time the table is redrawn.
+   */
+  private boolean headersDirty;
+
   private TableRowElement hoveringRow;
 
   /**
@@ -1200,6 +1208,11 @@
    * Modifications to the {@link ColumnSortList} will be reflected in the table
    * header.
    * 
+   * <p>
+   * Note that the implementation may redraw the headers on every modification
+   * to the {@link ColumnSortList}.
+   * </p>
+   * 
    * @return the {@link ColumnSortList}
    */
   public ColumnSortList getColumnSortList() {
@@ -1363,6 +1376,7 @@
     }
     CellBasedWidgetImpl.get().sinkEvents(this, consumedEvents);
 
+    headersDirty = true;
     refreshColumnsAndRedraw();
   }
 
@@ -1419,14 +1433,34 @@
   }
 
   /**
-   * Redraw the table's footers.
+   * Check if auto footer refresh is enabled or disabled
+   * 
+   * @return true if disabled, false if enabled
+   * @see #setAutoFooterRefreshDisabled(boolean)
+   */
+  public boolean isAutoFooterRefreshDisabled() {
+    return footerRefreshDisabled;
+  }
+
+  /**
+   * Check if auto header refresh is enabled or disabled
+   * 
+   * @return true if disabled, false if enabled
+   * @see #setAutoHeaderRefreshDisabled(boolean)
+   */
+  public boolean isAutoHeaderRefreshDisabled() {
+    return headerRefreshDisabled;
+  }
+
+  /**
+   * Redraw the table's footers. The footers will be re-rendered synchronously.
    */
   public void redrawFooters() {
     createHeaders(true);
   }
 
   /**
-   * Redraw the table's headers.
+   * Redraw the table's headers. The headers will be re-rendered synchronously.
    */
   public void redrawHeaders() {
     createHeaders(false);
@@ -1464,6 +1498,7 @@
     }
 
     // Redraw the table asynchronously.
+    headersDirty = true;
     refreshColumnsAndRedraw();
 
     // We don't unsink events because other handlers or user code may have sunk
@@ -1479,6 +1514,36 @@
   public abstract void removeColumnStyleName(int index, String styleName);
 
   /**
+   * Enable or disable auto footer refresh when row data is changed. By default,
+   * footers are refreshed every time the row data changes in case the headers
+   * depend on the current row data. If the headers do not depend on the current
+   * row data, you can disable this feature to improve performance.
+   * 
+   * <p>
+   * Note that headers will still refresh when columns are added or removed,
+   * regardless of whether or not this feature is enabled.
+   * </p>
+   */
+  public void setAutoFooterRefreshDisabled(boolean disabled) {
+    this.footerRefreshDisabled = disabled;
+  }
+
+  /**
+   * Enable or disable auto header refresh when row data is changed. By default,
+   * headers are refreshed every time the row data changes in case the footers
+   * depend on the current row data. If the footers do not depend on the current
+   * row data, you can disable this feature to improve performance.
+   * 
+   * <p>
+   * Note that footers will still refresh when columns are added or removed,
+   * regardless of whether or not this feature is enabled.
+   * </p>
+   */
+  public void setAutoHeaderRefreshDisabled(boolean disabled) {
+    this.headerRefreshDisabled = disabled;
+  }
+
+  /**
    * Set the width of a {@link Column}. The width will persist with the column
    * and takes precedence of any width set via
    * {@link #setColumnWidth(int, String)}.
@@ -1795,6 +1860,11 @@
           // TODO(jlabanca): Get visible col when custom headers are supported.
           Column<T, ?> column = col < columns.size() ? columns.get(col) : null;
           if (column != null && column.isSortable()) {
+            /*
+             * Force the headers to refresh the next time data is pushed so we
+             * update the sort icon in the header.
+             */
+            headersDirty = true;
             updatingSortList = true;
             sortList.push(column);
             updatingSortList = false;
@@ -2363,11 +2433,6 @@
     doSetHeaderVisible(isFooter, hasHeader);
   }
 
-  private void createHeadersAndFooters() {
-    createHeaders(false);
-    createHeaders(true);
-  }
-
   /**
    * Fire an event to the Cell within the specified {@link TableCellElement}.
    */
@@ -2589,7 +2654,14 @@
     }
 
     // Render the headers and footers.
-    createHeadersAndFooters();
+    boolean wereHeadersDirty = headersDirty;
+    headersDirty = false;
+    if (wereHeadersDirty || !headerRefreshDisabled) {
+      createHeaders(false);
+    }
+    if (wereHeadersDirty || !footerRefreshDisabled) {
+      createHeaders(true);
+    }
   }
 
   private <C> boolean resetFocusOnCellImpl(int row, int col, HasCell<T, C> column,
diff --git a/user/test/com/google/gwt/user/cellview/client/AbstractCellTableTestBase.java b/user/test/com/google/gwt/user/cellview/client/AbstractCellTableTestBase.java
index 9fe43da..487f431 100644
--- a/user/test/com/google/gwt/user/cellview/client/AbstractCellTableTestBase.java
+++ b/user/test/com/google/gwt/user/cellview/client/AbstractCellTableTestBase.java
@@ -747,6 +747,194 @@
     }
   }
 
+  public void testSetAutoFooterRefreshDisabled() {
+    AbstractCellTable<String> table = createAbstractHasData();
+    assertFalse(table.isAutoHeaderRefreshDisabled());
+    assertFalse(table.isAutoFooterRefreshDisabled());
+
+    table.setAutoFooterRefreshDisabled(true);
+    assertFalse(table.isAutoHeaderRefreshDisabled());
+    assertTrue(table.isAutoFooterRefreshDisabled());
+
+    /*
+     * Inserting a column should render the headers and footers, even if auto
+     * refresh is disabled.
+     */
+    final List<String> log = new ArrayList<String>();
+    Column<String, ?> col0 = new MockColumn<String, String>();
+    TextHeader header0 = new TextHeader("header0") {
+      @Override
+      public void render(Context context, SafeHtmlBuilder sb) {
+        super.render(context, sb);
+        log.add("header0 rendered");
+      }
+    };
+    TextHeader footer0 = new TextHeader("footer0") {
+      @Override
+      public void render(Context context, SafeHtmlBuilder sb) {
+        super.render(context, sb);
+        log.add("footer0 rendered");
+      }
+    };
+    table.addColumn(col0, header0, footer0);
+    assertEquals(0, log.size()); // Headers are rendered asynchronously.
+    table.getPresenter().flush(); // Force headers to render.
+    assertEquals("header0 rendered", log.remove(0));
+    assertEquals("footer0 rendered", log.remove(0));
+    assertEquals(0, log.size());
+
+    /*
+     * Inserting another column should render the headers and footers, even if
+     * auto refresh is disabled.
+     */
+    Column<String, ?> col1 = new MockColumn<String, String>();
+    TextHeader header1 = new TextHeader("header1") {
+      @Override
+      public void render(Context context, SafeHtmlBuilder sb) {
+        super.render(context, sb);
+        log.add("header1 rendered");
+      }
+    };
+    TextHeader footer1 = new TextHeader("footer1") {
+      @Override
+      public void render(Context context, SafeHtmlBuilder sb) {
+        super.render(context, sb);
+        log.add("footer1 rendered");
+      }
+    };
+    table.addColumn(col1, header1, footer1);
+    assertEquals(0, log.size()); // Headers are rendered asynchronously.
+    table.getPresenter().flush(); // Force headers to render.
+    assertEquals("header0 rendered", log.remove(0));
+    assertEquals("header1 rendered", log.remove(0));
+    assertEquals("footer0 rendered", log.remove(0));
+    assertEquals("footer1 rendered", log.remove(0));
+    assertEquals(0, log.size());
+
+    /*
+     * Removing a column should render the headers and footers, even if auto
+     * refresh is disabled.
+     */
+    table.removeColumn(col0);
+    assertEquals(0, log.size()); // Headers are rendered asynchronously.
+    table.getPresenter().flush(); // Force headers to render.
+    assertEquals("header1 rendered", log.remove(0));
+    assertEquals("footer1 rendered", log.remove(0));
+    assertEquals(0, log.size());
+
+    /*
+     * Setting data only causes footers to render if auto refresh is enabled,
+     * which it is not. Header refresh is still enabled.
+     */
+    populateData(table);
+    assertEquals(0, log.size()); // Headers are rendered asynchronously.
+    table.getPresenter().flush(); // Force headers to render.
+    assertEquals("header1 rendered", log.remove(0));
+    assertEquals(0, log.size());
+
+    /*
+     * Sorting a column forces the headers only to refresh. The footers are not
+     * refreshed.
+     */
+    table.getColumnSortList().push(col1);
+    assertEquals("header1 rendered", log.remove(0));
+    assertEquals(0, log.size());
+  }
+
+  public void testSetAutoHeaderRefreshDisabled() {
+    AbstractCellTable<String> table = createAbstractHasData();
+    assertFalse(table.isAutoHeaderRefreshDisabled());
+    assertFalse(table.isAutoFooterRefreshDisabled());
+
+    table.setAutoHeaderRefreshDisabled(true);
+    assertTrue(table.isAutoHeaderRefreshDisabled());
+    assertFalse(table.isAutoFooterRefreshDisabled());
+
+    /*
+     * Inserting a column should render the headers and footers, even if auto
+     * refresh is disabled.
+     */
+    final List<String> log = new ArrayList<String>();
+    Column<String, ?> col0 = new MockColumn<String, String>();
+    TextHeader header0 = new TextHeader("header0") {
+      @Override
+      public void render(Context context, SafeHtmlBuilder sb) {
+        super.render(context, sb);
+        log.add("header0 rendered");
+      }
+    };
+    TextHeader footer0 = new TextHeader("footer0") {
+      @Override
+      public void render(Context context, SafeHtmlBuilder sb) {
+        super.render(context, sb);
+        log.add("footer0 rendered");
+      }
+    };
+    table.addColumn(col0, header0, footer0);
+    assertEquals(0, log.size()); // Headers are rendered asynchronously.
+    table.getPresenter().flush(); // Force headers to render.
+    assertEquals("header0 rendered", log.remove(0));
+    assertEquals("footer0 rendered", log.remove(0));
+    assertEquals(0, log.size());
+
+    /*
+     * Inserting another column should render the headers and footers, even if
+     * auto refresh is disabled.
+     */
+    Column<String, ?> col1 = new MockColumn<String, String>();
+    TextHeader header1 = new TextHeader("header1") {
+      @Override
+      public void render(Context context, SafeHtmlBuilder sb) {
+        super.render(context, sb);
+        log.add("header1 rendered");
+      }
+    };
+    TextHeader footer1 = new TextHeader("footer1") {
+      @Override
+      public void render(Context context, SafeHtmlBuilder sb) {
+        super.render(context, sb);
+        log.add("footer1 rendered");
+      }
+    };
+    table.addColumn(col1, header1, footer1);
+    assertEquals(0, log.size()); // Headers are rendered asynchronously.
+    table.getPresenter().flush(); // Force headers to render.
+    assertEquals("header0 rendered", log.remove(0));
+    assertEquals("header1 rendered", log.remove(0));
+    assertEquals("footer0 rendered", log.remove(0));
+    assertEquals("footer1 rendered", log.remove(0));
+    assertEquals(0, log.size());
+
+    /*
+     * Removing a column should render the headers and footers, even if auto
+     * refresh is disabled.
+     */
+    table.removeColumn(col0);
+    assertEquals(0, log.size()); // Headers are rendered asynchronously.
+    table.getPresenter().flush(); // Force headers to render.
+    assertEquals("header1 rendered", log.remove(0));
+    assertEquals("footer1 rendered", log.remove(0));
+    assertEquals(0, log.size());
+
+    /*
+     * Setting data only causes headers to render if auto refresh is enabled,
+     * which it is not. Footer refresh is still enabled.
+     */
+    populateData(table);
+    assertEquals(0, log.size()); // Headers are rendered asynchronously.
+    table.getPresenter().flush(); // Force headers to render.
+    assertEquals("footer1 rendered", log.remove(0));
+    assertEquals(0, log.size());
+
+    /*
+     * Sorting a column forces the headers only to refresh. The footers are not
+     * refreshed.
+     */
+    table.getColumnSortList().push(col1);
+    assertEquals("header1 rendered", log.remove(0));
+    assertEquals(0, log.size());
+  }
+
   public void testSetColumnWidth() {
     AbstractCellTable<String> table = createAbstractHasData(new TextCell());
     Column<String, ?> col0 = new MockColumn<String, String>();