Adds basic support for headers and footers in the table view widget. In its
current form, it removes the next/prev-page buttons; I'll re-add those in a
separate pass.


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7696 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/bikeshed/cells/client/CheckboxCell.java b/bikeshed/src/com/google/gwt/bikeshed/cells/client/CheckboxCell.java
index fe98524..7920642 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/cells/client/CheckboxCell.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/cells/client/CheckboxCell.java
@@ -40,7 +40,7 @@
   @Override
   public void render(Boolean data, StringBuilder sb) {
     sb.append("<input type=\"checkbox\"");
-    if (data == true) {
+    if ((data != null) && (data == true)) {
       sb.append(" checked");
     }
     sb.append("/>");
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
index 90a9164..271cd97 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
@@ -15,8 +15,6 @@
  */
 package com.google.gwt.bikeshed.list.client;
 
-import com.google.gwt.bikeshed.cells.client.ButtonCell;
-import com.google.gwt.bikeshed.cells.client.ValueUpdater;
 import com.google.gwt.bikeshed.list.shared.ListEvent;
 import com.google.gwt.bikeshed.list.shared.ListHandler;
 import com.google.gwt.bikeshed.list.shared.ListModel;
@@ -31,6 +29,7 @@
 import com.google.gwt.dom.client.TableCellElement;
 import com.google.gwt.dom.client.TableElement;
 import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.TableSectionElement;
 import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Widget;
@@ -52,16 +51,27 @@
   private int totalSize;
   private List<Column<T, ?>> columns = new ArrayList<Column<T, ?>>();
   private ArrayList<T> data = new ArrayList<T>();
-  private ButtonCell prevButton = new ButtonCell();
-  private ButtonCell nextButton = new ButtonCell();
+
+  private List<Header<?>> headers = new ArrayList<Header<?>>();
+  private List<Header<?>> footers = new ArrayList<Header<?>>();
+
+  private TableElement table;
+  private TableSectionElement thead;
+  private TableSectionElement tfoot;
+  private TableSectionElement tbody;
 
   public PagingTableListView(ListModel<T> listModel, final int pageSize) {
     this.pageSize = pageSize;
-    setElement(Document.get().createTableElement());
+    setElement(table = Document.get().createTableElement());
+    thead = table.createTHead();
+    table.appendChild(tbody = Document.get().createTBodyElement());
+    tfoot = table.createTFoot();
     createRows();
 
-    // TODO: total hack.
-    sinkEvents(Event.MOUSEEVENTS | Event.KEYEVENTS);
+    // TODO: Total hack. It would almost definitely be preferable to sink only
+    // those events actually needed by cells.
+    sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.KEYEVENTS
+        | Event.ONCHANGE);
 
     // Attach to the list model.
     listReg = listModel.addListHandler(new ListHandler<T>() {
@@ -82,8 +92,19 @@
     listReg.setRangeOfInterest(0, pageSize);
   }
 
-  // TODO: remove(Column)
   public void addColumn(Column<T, ?> col) {
+    addColumn(col, null, null);
+  }
+
+  public void addColumn(Column<T, ?> col, Header<?> header) {
+    addColumn(col, header, null);
+  }
+
+  // TODO: remove(Column)
+  public void addColumn(Column<T, ?> col, Header<?> header, Header<?> footer) {
+    headers.add(header);
+    footers.add(footer);
+    createHeadersAndFooters();  // TODO: defer header recreation
     columns.add(col);
     createRows();
     setPage(curPage); // TODO: better way to refresh?
@@ -106,44 +127,29 @@
   public void onBrowserEvent(Event event) {
     EventTarget target = event.getEventTarget();
     Node node = Node.as(target);
-    while (node != null) {
-      if (Element.is(node)) {
-        Element elem = Element.as(node);
+    TableCellElement cell = findNearestParentCell(node);
+    if (cell == null) {
+      return;
+    }
 
-        // TODO: We need is() implementations in all Element subclasses.
-        String tagName = elem.getTagName();
-        if ("td".equalsIgnoreCase(tagName)) {
-          TableCellElement td = TableCellElement.as(elem);
-          TableRowElement tr = TableRowElement.as(td.getParentElement());
-
-          // TODO: row/col assertions.
-          int row = tr.getRowIndex(), col = td.getCellIndex();
-          if (row < pageSize) {
-            T value = data.get(row);
-            Column<T, ?> column = columns.get(col);
-            column.onBrowserEvent(elem, value, event);
-          } else if (row == pageSize) {
-            if (col == 0) {
-              prevButton.onBrowserEvent(elem, null, event,
-                  new ValueUpdater<String>() {
-                    public void update(String value) {
-                      previousPage();
-                    }
-                  });
-            } else if (col == 2) {
-              nextButton.onBrowserEvent(elem, null, event,
-                  new ValueUpdater<String>() {
-                    public void update(String value) {
-                      nextPage();
-                    }
-                  });
-            }
-          }
-          break;
-        }
+    TableRowElement tr = TableRowElement.as(cell.getParentElement());
+    TableSectionElement section = TableSectionElement.as(tr.getParentElement());
+    int col = cell.getCellIndex();
+    if (section == thead) {
+      Header<?> header = headers.get(col);
+      if (header != null) {
+        header.onBrowserEvent(cell, event);
       }
-
-      node = node.getParentNode();
+    } else if (section == tfoot) {
+      Header<?> footer = footers.get(col);
+      if (footer != null) {
+        footer.onBrowserEvent(cell, event);
+      }
+    } else if (section == tbody) {
+      int row = tr.getSectionRowIndex();
+      T value = data.get(row);
+      Column<T, ?> column = columns.get(col);
+      column.onBrowserEvent(cell, value, event);
     }
   }
 
@@ -187,11 +193,10 @@
   }
 
   protected void render(int start, int length, List<T> values) {
-    TableElement table = getElement().cast();
     int numCols = columns.size();
     int pageStart = curPage * pageSize;
 
-    NodeList<TableRowElement> rows = table.getRows();
+    NodeList<TableRowElement> rows = tbody.getRows();
     for (int r = start; r < start + length; ++r) {
       TableRowElement row = rows.getItem(r - pageStart);
       T q = values.get(r - start);
@@ -203,29 +208,47 @@
         columns.get(c).render(q, sb);
         cell.setInnerHTML(sb.toString());
 
-        // TODO: really total hack!
+        // TODO: Really total hack! There's gotta be a better way...
         Element child = cell.getFirstChildElement();
         if (child != null) {
-          Event.sinkEvents(child, Event.ONCHANGE | Event.ONFOCUS | Event.ONBLUR);
+          Event.sinkEvents(child, Event.ONFOCUS | Event.ONBLUR);
         }
       }
     }
   }
 
+  private void createHeaders(List<Header<?>> headers, TableSectionElement section) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("<tr>");
+    for (Header<?> header : headers) {
+      sb.append("<th>");
+      if (header != null) {
+        header.render(sb);
+      }
+      sb.append("</th>");
+    }
+    sb.append("</tr>");
+
+    section.setInnerHTML(sb.toString());
+  }
+
+  private void createHeadersAndFooters() {
+    createHeaders(headers, thead);
+    createHeaders(footers, tfoot);
+  }
+
   private void createRows() {
-    TableElement table = getElement().cast();
     int numCols = columns.size();
 
     // TODO - only delete as needed
-    int numRows = table.getRows().getLength();
+    int numRows = tbody.getRows().getLength();
     while (numRows-- > 0) {
-      table.deleteRow(0);
+      tbody.deleteRow(0);
     }
 
     for (int r = 0; r < pageSize; ++r) {
-      TableRowElement row = table.insertRow(0);
-      row.setClassName("pagingTableListView "
-          + ((r & 0x1) == 0 ? "evenRow" : "oddRow"));
+      TableRowElement row = tbody.insertRow(0);
+      row.setClassName("pagingTableListView " + ((r & 0x1) == 0 ? "evenRow" : "oddRow"));
 
       // TODO: use cloneNode() to make this even faster.
       for (int c = 0; c < numCols; ++c) {
@@ -233,26 +256,6 @@
       }
     }
 
-    // Add the final row containing paging buttons
-    TableRowElement pageRow = table.insertRow(pageSize);
-    pageRow.insertCell(0);
-    pageRow.insertCell(1);
-    pageRow.insertCell(2);
-
-    StringBuilder sb;
-
-    sb = new StringBuilder();
-    prevButton.render("Previous", sb);
-    pageRow.getCells().getItem(0).setInnerHTML(sb.toString());
-
-    pageRow.getCells().getItem(1).setAttribute("colspan", "" + (numCols - 2));
-    pageRow.getCells().getItem(1).setAttribute("align", "center");
-
-    sb = new StringBuilder();
-    nextButton.render("Next", sb);
-    pageRow.getCells().getItem(2).setInnerHTML(sb.toString());
-    pageRow.getCells().getItem(2).setAttribute("align", "right");
-
     // Make room for the data cache
     data.ensureCapacity(pageSize);
     while (data.size() < pageSize) {
@@ -260,24 +263,37 @@
     }
   }
 
+  private TableCellElement findNearestParentCell(Node node) {
+    while ((node != null) && (node != table)) {
+      if (Element.is(node)) {
+        Element elem = Element.as(node);
+
+        // TODO: We need is() implementations in all Element subclasses.
+        // This would allow us to use TableCellElement.is() -- much cleaner.
+        String tagName = elem.getTagName();
+        if ("td".equalsIgnoreCase(tagName) || "th".equalsIgnoreCase(tagName)) {
+          return elem.cast();
+        }
+      }
+      node = node.getParentNode();
+    }
+    return null;
+  }
+
   /**
    * Update the text that shows the current page.
    * 
    * @param page the current page
    */
   private void updatePageText(int page) {
-    TableElement table = getElement().cast();
-    NodeList<TableRowElement> rows = table.getRows();
-    String text = "Page " + (page + 1) + " of " + numPages;
-    rows.getItem(rows.getLength() - 1).getCells().getItem(1).setInnerText(text);
+    // TODO: Update external paging widget.
   }
 
   private void updateRowVisibility() {
     int visible = Math.min(pageSize, totalSize - curPage * pageSize);
 
-    TableElement table = getElement().cast();
     for (int r = 0; r < pageSize; ++r) {
-      Style rowStyle = table.getRows().getItem(r).getStyle();
+      Style rowStyle = tbody.getRows().getItem(r).getStyle();
       if (r < visible) {
         rowStyle.clearDisplay();
       } else {
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
index e1dba47..aaef117 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
@@ -24,6 +24,7 @@
 import com.google.gwt.bikeshed.list.shared.SizeChangeEvent;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Node;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Widget;
 
@@ -113,19 +114,25 @@
     Element parent = getElement();
     int childCount = parent.getChildCount();
 
-    // Update existing cells with new values.
-    int i, existing = Math.min(len, childCount);
-    for (i = start; i < existing; ++i) {
-      Element elem = parent.getChild(i).cast();
-      cell.setValue(elem, values.get(i));
+    // Create innerHTML for the new items.
+    int end = start + len;
+    StringBuilder html = new StringBuilder();
+
+    // Empty items to fill any gaps.
+    int totalToAdd = 0;
+    for (int i = childCount; i < start; ++i) {
+      html.append("<div __idx='" + i + "'>");
+      cell.render(null, html);
+      html.append("</div>");
+      ++totalToAdd;
     }
 
-    // Create new cells if necessary.
-    StringBuilder html = new StringBuilder();
-    for (; i < len; ++i) {
+    // Items rendered from data.
+    for (int i = start; i < end; ++i) {
       html.append("<div __idx='" + i + "'>");
-      cell.render(values.get(i), html);
+      cell.render(values.get(i - start), html);
       html.append("</div>");
+      ++totalToAdd;
     }
 
     if (childCount == 0) {
@@ -136,9 +143,27 @@
       // in a temporary element, then move the cells back to the main element.
       tmpElem.setInnerHTML(html.toString());
 
+      // Clear out old cells that overlap the new cells.
+      if (start < childCount) {
+        int toRemove = Math.min(end, childCount) - start;
+        for (int i = 0; i < toRemove; ++i) {
+          parent.removeChild(parent.getChild(start));
+        }
+        childCount = parent.getChildCount();
+      }
+
       // Move the new cells over from the temp element.
-      for (i = 0; i < len - childCount; ++i) {
-        parent.appendChild(tmpElem.getChild(0));
+      if (start >= childCount) {
+        // Just append to the end.
+        for (int i = 0; i < totalToAdd; ++i) {
+          parent.appendChild(tmpElem.getChild(0));
+        }
+      } else {
+        // Insert them in the middle somewhere.
+        Node before = parent.getChild(start);
+        for (int i = 0; i < totalToAdd; ++i) {
+          parent.insertBefore(tmpElem.getChild(0), before);
+        }
       }
     }
   }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java
index 13f44e0..3fb325e 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockQueryWidget.java
@@ -16,6 +16,7 @@
 package com.google.gwt.bikeshed.sample.stocks.client;
 
 import com.google.gwt.bikeshed.list.client.PagingTableListView;
+import com.google.gwt.bikeshed.list.client.TextHeader;
 import com.google.gwt.bikeshed.list.shared.ListModel;
 import com.google.gwt.bikeshed.sample.stocks.shared.StockQuote;
 import com.google.gwt.dom.client.Style.Unit;
@@ -40,9 +41,9 @@
     // Create the results table.
     resultsTable = new PagingTableListView<StockQuote>(searchListModel, 10);
     resultsTable.addColumn(Columns.favoriteColumn);
-    resultsTable.addColumn(Columns.tickerColumn);
-    resultsTable.addColumn(Columns.nameColumn);
-    resultsTable.addColumn(Columns.priceColumn);
+    resultsTable.addColumn(Columns.tickerColumn, new TextHeader("ticker"));
+    resultsTable.addColumn(Columns.nameColumn, new TextHeader("name"));
+    resultsTable.addColumn(Columns.priceColumn, new TextHeader("price"));
     resultsTable.addColumn(Columns.buyColumn);
    
     // Focus the cursor on the name field when the app loads
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
index ad5b85a..a16284e 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.bikeshed.cells.client.FieldUpdater;
 import com.google.gwt.bikeshed.list.client.PagingTableListView;
+import com.google.gwt.bikeshed.list.client.TextHeader;
 import com.google.gwt.bikeshed.list.shared.AsyncListModel;
 import com.google.gwt.bikeshed.list.shared.ListListModel;
 import com.google.gwt.bikeshed.list.shared.Range;
@@ -135,10 +136,10 @@
 
     // Create the favorites table.
     favoritesTable = new PagingTableListView<StockQuote>(favoritesListModel, 10);
-    favoritesTable.addColumn(Columns.tickerColumn);
-    favoritesTable.addColumn(Columns.priceColumn);
-    favoritesTable.addColumn(Columns.sharesColumn);
-    favoritesTable.addColumn(Columns.dollarsColumn);
+    favoritesTable.addColumn(Columns.tickerColumn, new TextHeader("ticker"));
+    favoritesTable.addColumn(Columns.priceColumn, new TextHeader("price"));
+    favoritesTable.addColumn(Columns.sharesColumn, new TextHeader("shares"));
+    favoritesTable.addColumn(Columns.dollarsColumn, new TextHeader("value"));
     favoritesTable.addColumn(Columns.buyColumn);
     favoritesTable.addColumn(Columns.sellColumn);