Add a demo of SimpleCellList and fix some bugs found by the demo

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

Review by: jlabanca@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7794 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 0d23a0f..ed7eea3 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
@@ -25,7 +25,6 @@
 import com.google.gwt.dom.client.DivElement;
 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.dom.client.Style.Display;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Widget;
@@ -43,30 +42,40 @@
   private final Cell<T, Void> cell;
   private final ArrayList<T> data = new ArrayList<T>();
   private int increment;
+  private int initialMaxSize;
   private int maxSize;
   private ListModel<T> model;
-  private final Element showMoreElem;
-  private final Element tmpElem;
   private ListRegistration reg;
+  private int seq; // for debugging - TODO: remove
+  private final Element showFewerElem;
+  private final Element showMoreElem;
+  private int size;
+  private final Element tmpElem;
   private ValueUpdater<T, Void> valueUpdater;
-  
   public SimpleCellList(ListModel<T> model, Cell<T, Void> cell, int maxSize,
       int increment) {
-    this.maxSize = maxSize;
+    this.initialMaxSize = this.maxSize = maxSize;
     this.increment = increment;
     this.model = model;
     this.cell = cell;
+    this.seq = 0;
     
     tmpElem = Document.get().createDivElement();
     
     showMoreElem = Document.get().createDivElement();
-    showMoreElem.setInnerHTML("<i>Show " + increment + " more</i>");
-    showMoreElem.getStyle().setDisplay(Display.NONE);
+    showMoreElem.setInnerHTML("<button>Show more</button>");
+
+    showFewerElem = Document.get().createDivElement();
+    showFewerElem.setInnerHTML("<button>Show fewer</button>");
+
+    showOrHide(showMoreElem, false);
+    showOrHide(showFewerElem, false);
 
     // TODO: find some way for cells to communicate what they're interested in.
     DivElement outerDiv = Document.get().createDivElement();
     DivElement innerDiv = Document.get().createDivElement();
     outerDiv.appendChild(innerDiv);
+    outerDiv.appendChild(showFewerElem);
     outerDiv.appendChild(showMoreElem);
     setElement(outerDiv);
     sinkEvents(Event.ONCLICK);
@@ -76,14 +85,22 @@
   @Override
   public void onBrowserEvent(Event event) {
     Element target = event.getEventTarget().cast();
-    String idxString = "";
-    while ((target != null)
-        && ((idxString = target.getAttribute("__idx")).length() == 0)) {
-      target = target.getParentElement();
-    }
-    if (idxString.length() > 0) {
-      int idx = Integer.parseInt(idxString);
-      cell.onBrowserEvent(target, data.get(idx), null, event, valueUpdater);
+    if (target.getParentElement() == showMoreElem) {
+      this.maxSize += increment;
+      reg.setRangeOfInterest(0, maxSize);
+    } else if (target.getParentElement() == showFewerElem) {
+      this.maxSize = Math.max(initialMaxSize, maxSize - increment);
+      reg.setRangeOfInterest(0, maxSize);
+    } else {
+      String idxString = "";
+      while ((target != null)
+          && ((idxString = target.getAttribute("__idx")).length() == 0)) {
+        target = target.getParentElement();
+      }
+      if (idxString.length() > 0) {
+        int idx = Integer.parseInt(idxString);
+        cell.onBrowserEvent(target, data.get(idx), null, event, valueUpdater);
+      }
     }
   }
   
@@ -98,38 +115,64 @@
     // Register for model events.
     this.reg = model.addListHandler(new ListHandler<T>() {
       public void onDataChanged(ListEvent<T> event) {
-        int start = event.getStart(), len = event.getLength();
+        int start = event.getStart();
+        int len = event.getLength();
         List<T> values = event.getValues();
-        for (int i = 0; i < len; ++i) {
-          data.set(start + i, values.get(i));
+
+        // Construct a run of element from start (inclusive) to start + len (exclusive)
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < len; i++) {
+          sb.append("<div __idx='" + (start + i) + "' __seq='" + seq++ + "'>");
+          cell.render(values.get(i), null, sb);
+          sb.append("</div>");
         }
-        render(start, len, values);
+
+        Element parent = getElement().getFirstChildElement();
+        if (start == 0 && len == maxSize) {
+          parent.setInnerHTML(sb.toString());
+        } else {
+          makeElements();
+          tmpElem.setInnerHTML(sb.toString());
+          for (int i = 0; i < len; i++) {
+            Element child = parent.getChild(start + i).cast();
+            parent.replaceChild(tmpElem.getChild(0), child);
+          }
+        }
       }
 
       public void onSizeChanged(SizeChangeEvent event) {
-        int size = event.getSize();
-        if (size > maxSize) {
-          showMoreElem.getStyle().clearDisplay();
-        } else {
-          showMoreElem.getStyle().setDisplay(Display.NONE);
-        }
+        size = event.getSize();
+        showOrHide(showMoreElem, size > maxSize);
+        showOrHide(showFewerElem, maxSize > initialMaxSize);
+      }
+
+      private void makeElements() {
+        Element parent = getElement().getFirstChildElement();
+        int childCount = parent.getChildCount();
         
-        int dataSize = data.size();
-        if (size < dataSize) {
-          while (size < dataSize) {
-            data.remove(dataSize - 1);
-            dataSize--;
+        int actualSize = Math.min(size, maxSize);
+        if (actualSize > childCount) {
+          // Create new elements with a "loading..." message
+          StringBuilder sb = new StringBuilder();
+          int newElements = actualSize - childCount;
+          for (int i = 0; i < newElements; i++) {
+            sb.append("<div __idx='" + (childCount + i) + "'><i>loading...</i></div>");
           }
-        } else {
-          data.ensureCapacity(size);
-          while (dataSize < size) {
-            data.add(null);
-            dataSize++;
+
+          if (childCount == 0) {
+            parent.setInnerHTML(sb.toString());
+          } else {
+            tmpElem.setInnerHTML(sb.toString());
+            for (int i = 0; i < newElements; i++) {
+              parent.appendChild(tmpElem.getChild(0));
+            }
+          }
+        } else if (actualSize < childCount) {
+          // Remove excess elements
+          while (actualSize < childCount) {
+            parent.getChild(--childCount).removeFromParent();
           }
         }
-        
-        // TODO: This only grows. It needs to shrink as well.
-        gc(size);
       }
     });
 
@@ -143,69 +186,11 @@
     this.reg = null;
   }
 
-  private void gc(int size) {
-    // Remove unused children if the size shrinks.
-    int childCount = getElement().getChildCount();
-    while (size < childCount) {
-      getElement().getChild(--childCount).removeFromParent();
-    }
-  }
-
-  private void render(int start, int len, List<T> values) {
-    Element parent = getElement().getFirstChildElement();
-    int childCount = parent.getChildCount();
-
-    // 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, null, html);
-      html.append("</div>");
-      ++totalToAdd;
-    }
-
-    // Items rendered from data.
-    for (int i = start; i < end; ++i) {
-      html.append("<div __idx='" + i + "'>");
-      cell.render(values.get(i - start), null, html);
-      html.append("</div>");
-      ++totalToAdd;
-    }
-
-    if (childCount == 0) {
-      // Fast path: No cells existed, so we can just user innerHTML.
-      parent.setInnerHTML(html.toString());
+  private void showOrHide(Element element, boolean show) {
+    if (show) {
+      element.getStyle().clearDisplay();
     } else {
-      // Slower path: We can't clobber the existing cells, so we use innerHTML
-      // 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.
-      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);
-        }
-      }
+      element.getStyle().setDisplay(Display.NONE);
     }
   }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListListModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListListModel.java
index 574230f..b9c1a11 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListListModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListListModel.java
@@ -53,8 +53,14 @@
           curSize = newSize;
           updateDataSize(curSize, true);
         }
-        updateViewData(0, list.size(), list);
         
+        if (modified) {
+          int length = maxModified - minModified;
+          updateViewData(minModified, length, list.subList(minModified, maxModified));
+          modified = false;
+        }
+        minModified = Integer.MAX_VALUE;
+        maxModified = Integer.MIN_VALUE;
         flushPending = false;
       }
     };
@@ -69,29 +75,72 @@
      */
     private List<T> list;
 
+    /**
+     * If modified is true, the smallest modified index.
+     */
+    private int maxModified;
+
+    /**
+     * If modified is true, one past the largest modified index.
+     */
+    private int minModified;
+
+    /**
+     * True if the list data has been modified.
+     */
+    private boolean modified;
+
     public ListWrapper(List<T> list) {
       this.list = list;
+      minModified = 0;
+      maxModified = list.size();
+      modified = true;
     }
 
     public void add(int index, T element) {
-      list.add(index, element);
-      flush();
+      try {
+        list.add(index, element);
+        minModified = Math.min(minModified, index);
+        maxModified = size();
+        modified = true;
+        flush();
+      } catch (IndexOutOfBoundsException e) {
+        throw new IndexOutOfBoundsException(e.getMessage());
+      }
     }
 
     public boolean add(T e) {
-      return flush(list.add(e));
+      boolean toRet = list.add(e);
+      minModified = Math.min(minModified, size() - 1);
+      maxModified = size();
+      modified = true;
+      return flush(toRet);
     }
 
     public boolean addAll(Collection<? extends T> c) {
-      return flush(list.addAll(c));
+      minModified = Math.min(minModified, size());
+      boolean toRet = list.addAll(c);
+      maxModified = size();
+      modified = true;
+      return flush(toRet);
     }
 
     public boolean addAll(int index, Collection<? extends T> c) {
-      return flush(list.addAll(index, c));
+      try {
+        boolean toRet = list.addAll(index, c);
+        minModified = Math.min(minModified, index);
+        maxModified = size();
+        modified = true;
+        return flush(toRet);
+      } catch (IndexOutOfBoundsException e) {
+        throw new IndexOutOfBoundsException(e.getMessage());
+      }
     }
 
     public void clear() {
       list.clear();
+      minModified = maxModified = 0;
+      modified = true;
       flush();
     }
 
@@ -145,25 +194,48 @@
     }
 
     public T remove(int index) {
-      T toRet = list.remove(index);
-      flush();
-      return toRet;
+      try {
+        T toRet = list.remove(index);
+        minModified = Math.min(minModified, index);
+        maxModified = size();
+        modified = true;
+        flush();
+        return toRet;
+      } catch (IndexOutOfBoundsException e) {
+        throw new IndexOutOfBoundsException(e.getMessage());
+      }
     }
 
     public boolean remove(Object o) {
-      return flush(list.remove(o));
+      int index = indexOf(o);
+      if (index == -1) {
+        return false;
+      }
+      remove(index);
+      return true;
     }
 
     public boolean removeAll(Collection<?> c) {
-      return flush(list.removeAll(c));
+      boolean toRet = list.removeAll(c);
+      minModified = 0;
+      maxModified = size();
+      modified = true;
+      return flush(toRet);
     }
 
     public boolean retainAll(Collection<?> c) {
-      return flush(list.retainAll(c));
+      boolean toRet = list.retainAll(c);
+      minModified = 0;
+      maxModified = size();
+      modified = true;
+      return flush(toRet);
     }
 
     public T set(int index, T element) {
       T toRet = list.set(index, element);
+      minModified = Math.min(minModified, index);
+      maxModified = Math.max(maxModified, index + 1);
+      modified = true;
       flush();
       return toRet;
     }
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/simplecelllist/SimpleCellList.gwt.xml b/bikeshed/src/com/google/gwt/sample/bikeshed/simplecelllist/SimpleCellList.gwt.xml
new file mode 100644
index 0000000..9ac9c97
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/simplecelllist/SimpleCellList.gwt.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Could not determine the version of your GWT SDK; using the module DTD from GWT 1.6.4. You may want to change this. -->
+<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.4//EN" "http://google-web-toolkit.googlecode.com/svn/tags/1.6.4/distro-source/core/src/gwt-module.dtd">
+<module rename-to='simplecelllist'>
+	<inherits name="com.google.gwt.bikeshed.list.List" />
+	<source path="client" />
+	<entry-point
+		class="com.google.gwt.sample.bikeshed.simplecelllist.client.SimpleCellListSample">
+	</entry-point>
+</module>
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/simplecelllist/client/SimpleCellListSample.java b/bikeshed/src/com/google/gwt/sample/bikeshed/simplecelllist/client/SimpleCellListSample.java
new file mode 100644
index 0000000..4fb0590
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/simplecelllist/client/SimpleCellListSample.java
@@ -0,0 +1,55 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.bikeshed.simplecelllist.client;
+
+import com.google.gwt.bikeshed.cells.client.TextCell;
+import com.google.gwt.bikeshed.list.client.SimpleCellList;
+import com.google.gwt.bikeshed.list.shared.ListListModel;
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.RootPanel;
+
+import java.util.List;
+
+/**
+ * SimpleCellList demo.
+ */
+public class SimpleCellListSample implements EntryPoint {
+
+  public void onModuleLoad() {
+    ListListModel<String> listModel = new ListListModel<String>();
+    final List<String> list = listModel.getList();
+    for (int i = 0; i < 50; i++) {
+      list.add("" + (i * 1000));
+    }
+
+    SimpleCellList<String> simpleCellList = new SimpleCellList<String>(listModel, new TextCell(), 10, 5);
+
+    RootPanel.get().add(simpleCellList);
+
+    new Timer() {
+      int index = 0;
+
+      @Override
+      public void run() {
+          list.set(index, "" + (Integer.parseInt(list.get(index)) + 1));
+          list.set(index + 15, "" + (Integer.parseInt(list.get(index + 15)) + 1));
+          index = (index + 1) % 10;
+          schedule(100);
+      }
+    }.schedule(100);
+  }
+}
diff --git a/bikeshed/war/SimpleCellList.html b/bikeshed/war/SimpleCellList.html
new file mode 100644
index 0000000..3119975
--- /dev/null
+++ b/bikeshed/war/SimpleCellList.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+    <title>Simple Cell List Demo</title>
+    <script type="text/javascript" language="javascript" src="simplecelllist/simplecelllist.nocache.js"></script>
+  </head>
+
+  <body>
+    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
+
+  </body>
+</html>