Implement a selection column, sortable columns, and row hovering in MailRecipe

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

Review by: jgw@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7936 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/bikeshed/cells/client/ClickableTextCell.java b/bikeshed/src/com/google/gwt/bikeshed/cells/client/ClickableTextCell.java
new file mode 100644
index 0000000..3a1e872
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/cells/client/ClickableTextCell.java
@@ -0,0 +1,56 @@
+/*
+ * 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.bikeshed.cells.client;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+
+/**
+ * A {@link Cell} used to render text.  Clicking on the call causes its
+ * @{link ValueUpdater} to be called.
+ */
+public class ClickableTextCell extends Cell<String, Void> {
+
+  private static ClickableTextCell instance;
+
+  public static ClickableTextCell getInstance() {
+    if (instance == null) {
+      instance = new ClickableTextCell();
+    }
+    return instance;
+  }
+
+  private ClickableTextCell() {
+  }
+
+  @Override
+  public Void onBrowserEvent(Element parent, String value, Void viewData,
+      NativeEvent event, ValueUpdater<String, Void> valueUpdater) {
+    String type = event.getType();
+    System.out.println(type);
+    if (type.equals("click")) {
+      valueUpdater.update(value, null);
+    }
+    return null;
+  }
+
+  @Override
+  public void render(String value, Void viewData, StringBuilder sb) {
+    if (value != null) {
+      sb.append(value);
+    }
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/Column.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/Column.java
index e3f6ba4..01b41d3 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/Column.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/Column.java
@@ -90,8 +90,7 @@
   }
 
   public void render(T object, StringBuilder sb) {
-    C value = getValue(object);
-    cell.render(value, viewDataMap.get(object), sb);
+    cell.render(getValue(object), viewDataMap.get(object), sb);
   }
 
   public void setFieldUpdater(FieldUpdater<T, C, V> fieldUpdater) {
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/Header.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/Header.java
index 37a5f29..35f2936 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/Header.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/Header.java
@@ -25,32 +25,31 @@
  *
  * @param <H> the {#link Cell} type
  */
-public class Header<H> {
+public abstract class Header<H> {
+
   private final Cell<H, Void> cell;
+
   private ValueUpdater<H, Void> updater;
-  private H value;
 
   public Header(Cell<H, Void> cell) {
     this.cell = cell;
   }
 
-  public H getValue() {
-    return value;
+  public boolean dependsOnSelection() {
+    return false;
   }
 
+  public abstract H getValue();
+
   public void onBrowserEvent(Element elem, NativeEvent event) {
-    cell.onBrowserEvent(elem, value, null, event, updater);
+    cell.onBrowserEvent(elem, getValue(), null, event, updater);
   }
 
   public void render(StringBuilder sb) {
-    cell.render(value, null, sb);
+    cell.render(getValue(), null, sb);
   }
 
   public void setUpdater(ValueUpdater<H, Void> updater) {
     this.updater = updater;
   }
-
-  public void setValue(H value) {
-    this.value = value;
-  }
 }
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 703f902..48048be 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
@@ -60,14 +60,13 @@
   private Delegate<T> delegate;
   private List<Header<?>> footers = new ArrayList<Header<?>>();
   private List<Header<?>> headers = new ArrayList<Header<?>>();
+  private TableRowElement hoveringRow;
   private int numPages;
   private int pageSize;
   private ProvidesKey<T> providesKey;
   private HandlerRegistration selectionHandler;
   private SelectionModel<T> selectionModel;
-
   private TableElement table;
-
   private TableSectionElement tbody;
   private TableSectionElement tfoot;
   private TableSectionElement thead;
@@ -171,6 +170,18 @@
       }
     } else if (section == tbody) {
       int row = tr.getSectionRowIndex();
+
+      if (event.getType().equals("mouseover")) {
+        if (hoveringRow != null) {
+          hoveringRow.removeClassName("hover");
+        }
+        hoveringRow = tr;
+        tr.addClassName("hover");
+      } else if (event.getType().equals("mouseout")) {
+        hoveringRow = null;
+        tr.removeClassName("hover");
+      }
+
       T value = data.get(row);
       Column<T, ?, ?> column = columns.get(col);
 
@@ -205,6 +216,18 @@
   }
 
   public void refreshSelection() {
+    // Refresh headers
+    Element th = thead.getFirstChild().getFirstChild().cast();
+    for (Header<?> header : headers) {
+      if (header.dependsOnSelection()) {
+        StringBuilder sb = new StringBuilder();
+        header.render(sb);
+        th.setInnerHTML(sb.toString());
+      }
+      th = th.getNextSibling().cast();
+    }
+
+    // Refresh body
     NodeList<TableRowElement> rows = tbody.getRows();
     for (int indexOnPage = 0; indexOnPage < pageSize; indexOnPage++) {
       TableRowElement row = rows.getItem(indexOnPage);
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/TextHeader.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/TextHeader.java
index cc59ef7..0890c3b 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/TextHeader.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/TextHeader.java
@@ -22,8 +22,15 @@
  */
 public class TextHeader extends Header<String> {
 
+  private String text;
+
   public TextHeader(String text) {
     super(TextCell.getInstance());
-    setValue(text);
+    this.text = text;
+  }
+
+  @Override
+  public String getValue() {
+    return text;
   }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListViewAdapter.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListViewAdapter.java
index 4124ab0..1d862c3 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListViewAdapter.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListViewAdapter.java
@@ -33,6 +33,53 @@
  */
 public class ListViewAdapter<T> extends AbstractListViewAdapter<T> {
 
+  private class WrappedListIterator implements ListIterator<T> {
+
+    int index;
+    private ListWrapper wrapper;
+
+    public WrappedListIterator(ListWrapper listWrapper, int index) {
+      this.wrapper = listWrapper;
+      this.index = index;
+    }
+
+    public void add(T o) {
+      throw new UnsupportedOperationException();
+    }
+
+    public boolean hasNext() {
+      return index < wrapper.size();
+    }
+
+    public boolean hasPrevious() {
+      return index > 0;
+    }
+
+    public T next() {
+      return wrapper.get(index++);
+    }
+
+    public int nextIndex() {
+      return index;
+    }
+
+    public T previous() {
+      return wrapper.get(--index);
+    }
+
+    public int previousIndex() {
+      return index - 1;
+    }
+
+    public void remove() {
+      throw new UnsupportedOperationException();
+    }
+
+    public void set(T o) {
+      wrapper.set(index, o);
+    }
+  }
+
   /**
    * A wrapper around a list that updates the model on any change.
    */
@@ -188,13 +235,11 @@
     }
 
     public ListIterator<T> listIterator() {
-      // TODO(jlabanca): Wrap the iterator
-      return list.listIterator();
+      return new WrappedListIterator(this, 0);
     }
 
     public ListIterator<T> listIterator(int index) {
-      // TODO(jlabanca): Wrap the iterator
-      return list.listIterator(index);
+      return new WrappedListIterator(this, index);
     }
 
     public T remove(int index) {
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/EditableTableRecipe.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/EditableTableRecipe.java
index 11b5e49..00b555b 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/EditableTableRecipe.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/EditableTableRecipe.java
@@ -61,8 +61,12 @@
     }
 
     EditTextColumn column = new EditTextColumn();
-    Header<String> header = new Header<String>(TextCell.getInstance());
-    header.setValue("<b>item</b>");
+    Header<String> header = new Header<String>(TextCell.getInstance()) {
+      @Override
+      public String getValue() {
+        return "<b>item</b>";
+      }
+    };
     table.addColumn(column, header);
 
     column.setFieldUpdater(new FieldUpdater<String, String, String>() {
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MailRecipe.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MailRecipe.java
index a099a0a..de642da 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MailRecipe.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MailRecipe.java
@@ -16,13 +16,17 @@
 package com.google.gwt.sample.bikeshed.cookbook.client;
 
 import com.google.gwt.bikeshed.cells.client.ButtonCell;
+import com.google.gwt.bikeshed.cells.client.Cell;
 import com.google.gwt.bikeshed.cells.client.CheckboxCell;
-import com.google.gwt.bikeshed.cells.client.DatePickerCell;
+import com.google.gwt.bikeshed.cells.client.ClickableTextCell;
+import com.google.gwt.bikeshed.cells.client.DateCell;
 import com.google.gwt.bikeshed.cells.client.FieldUpdater;
+import com.google.gwt.bikeshed.cells.client.TextCell;
+import com.google.gwt.bikeshed.cells.client.ValueUpdater;
 import com.google.gwt.bikeshed.list.client.Column;
+import com.google.gwt.bikeshed.list.client.Header;
 import com.google.gwt.bikeshed.list.client.PagingTableListView;
 import com.google.gwt.bikeshed.list.client.SimpleColumn;
-import com.google.gwt.bikeshed.list.client.TextColumn;
 import com.google.gwt.bikeshed.list.shared.DefaultSelectionModel;
 import com.google.gwt.bikeshed.list.shared.ListViewAdapter;
 import com.google.gwt.bikeshed.list.shared.ProvidesKey;
@@ -39,6 +43,8 @@
 import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwt.user.client.ui.Widget;
 
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -51,6 +57,10 @@
  */
 public class MailRecipe extends Recipe implements ClickHandler {
 
+  static interface GetValue<T, C> {
+    C getValue(T object);
+  }
+
   static class MailSelectionModel extends DefaultSelectionModel<Message> {
     enum Type {
       ALL(), NONE(), READ(), SENDER(), SUBJECT(), UNREAD();
@@ -77,6 +87,10 @@
       return keyProvider;
     }
 
+    public String getType() {
+      return type.toString();
+    }
+
     @Override
     public boolean isDefaultSelected(Message object) {
       switch (type) {
@@ -165,11 +179,11 @@
 
   // Hashing, comparison, and equality are based on the message id
   static class Message {
+    Date date;
     int id;
     boolean isRead;
     String sender;
     String subject;
-    Date date;
 
     public Message(int id, String sender, String subject, Date date) {
       super();
@@ -229,14 +243,14 @@
       "Kaan Boulier", "Emilee Naoma", "Atino Alice", "Debby Renay",
       "Versie Nereida", "Ramon Erikson", "Karole Crissy", "Nelda Olsen",
       "Mariana Dann", "Reda Cheyenne", "Edelmira Jody", "Agueda Shante",
-      "Marla Dorris"
-  };
+      "Marla Dorris"};
 
   private static final String[] subjects = {
       "GWT rocks", "What's a widget?", "Money in Nigeria",
       "Impress your colleagues with bling-bling", "Degree available",
-      "Rolex Watches", "Re: Re: yo bud", "Important notice"
-  };
+      "Rolex Watches", "Re: Re: yo bud", "Important notice"};
+
+  private List<Message> messages;
 
   private MailSelectionModel selectionModel = new MailSelectionModel();
 
@@ -264,7 +278,8 @@
   @Override
   protected Widget createWidget() {
     ListViewAdapter<Message> adapter = new ListViewAdapter<Message>();
-    List<Message> messages = adapter.getList();
+    messages = adapter.getList();
+
     Date now = new Date();
     Random rand = new Random();
     for (int i = 0; i < 1000; i++) {
@@ -272,60 +287,82 @@
       long dateOffset = rand.nextInt(60 * 60 * 24 * 90) * 1000L;
       Message message = new Message(10000 + i,
           senders[rand.nextInt(senders.length)],
-          subjects[rand.nextInt(subjects.length)],
-          new Date(now.getTime() - dateOffset));
+          subjects[rand.nextInt(subjects.length)], new Date(now.getTime()
+              - dateOffset));
       message.isRead = rand.nextBoolean();
       messages.add(message);
     }
 
+    final Comparator<Message> idComparator = new Comparator<Message>() {
+      public int compare(Message o1, Message o2) {
+        // Integer comparison
+        return o1.id - o2.id;
+      }
+    };
+
+    final Comparator<Message> dateComparator = new Comparator<Message>() {
+      public int compare(Message o1, Message o2) {
+        long cmp = o1.date.getTime() - o2.date.getTime();
+        if (cmp < 0) {
+          return -1;
+        } else if (cmp > 0) {
+          return 1;
+        } else {
+          return 0;
+        }
+      }
+    };
+
+    sortMessages(idComparator, true);
+
     table = new PagingTableListView<Message>(adapter, 10);
     table.setSelectionModel(selectionModel);
     adapter.addView(table);
 
-    // The state of the checkbox is taken from the selection model
-    SimpleColumn<Message, Boolean> selectedColumn = new SimpleColumn<Message, Boolean>(
-        new CheckboxCell()) {
+    // The state of the checkbox is synchronized with the selection model
+    SelectionColumn<Message> selectedColumn = new SelectionColumn<Message>(
+        selectionModel);
+    Header<Boolean> selectedHeader = new Header<Boolean>(new CheckboxCell()) {
       @Override
       public boolean dependsOnSelection() {
         return true;
       }
 
       @Override
-      public Boolean getValue(Message object) {
-        return selectionModel.isSelected(object);
+      public Boolean getValue() {
+        return selectionModel.getType().equals("ALL");
       }
     };
-    // Update the selection model when the checkbox is changed manually
-    selectedColumn.setFieldUpdater(new FieldUpdater<Message, Boolean, Void>() {
-      public void update(int index, Message object, Boolean value, Void viewData) {
-        selectionModel.setSelected(object, value);
+    selectedHeader.setUpdater(new ValueUpdater<Boolean, Void>() {
+      public void update(Boolean value, Void viewData) {
+        if (value == true) {
+          selectionModel.setType("ALL");
+        } else if (value == false) {
+          selectionModel.setType("NONE");
+        }
       }
     });
-    table.addColumn(selectedColumn, "Selected");
+    table.addColumn(selectedColumn, selectedHeader);
 
-    TextColumn<Message> idColumn = new TextColumn<Message>() {
-      @Override
-      public String getValue(Message object) {
-        return "" + object.id;
-      }
-    };
-    table.addColumn(idColumn, "ID");
+    addColumn(table, "ID", TextCell.getInstance(),
+        new GetValue<Message, String>() {
+          public String getValue(Message object) {
+            return "" + object.id;
+          }
+        }, idComparator);
 
-    TextColumn<Message> isReadColumn = new TextColumn<Message>() {
-      @Override
+    addColumn(table, "Read", new GetValue<Message, String>() {
       public String getValue(Message object) {
         return object.isRead ? "read" : "unread";
       }
-    };
-    table.addColumn(isReadColumn, "Read");
+    });
 
-    Column<Message, Date, Void> dateColumn =
-      new Column<Message, Date, Void>(new DatePickerCell<Void>()) {
-        @Override
-        public Date getValue(Message object) {
-          return object.getDate();
-        }
-    };
+    Column<Message, Date, Void> dateColumn = addColumn(table, "Date",
+        new DateCell(), new GetValue<Message, Date>() {
+          public Date getValue(Message object) {
+            return object.date;
+          }
+        }, dateComparator);
     dateColumn.setFieldUpdater(new FieldUpdater<Message, Date, Void>() {
       public void update(int index, Message object, Date value, Void viewData) {
         Window.alert("Changed date from " + object.date + " to " + value);
@@ -333,26 +370,21 @@
         table.refresh();
       }
     });
-    table.addColumn(dateColumn, "Date");
 
-    TextColumn<Message> senderColumn = new TextColumn<Message>() {
-      @Override
+    addColumn(table, "Sender", new GetValue<Message, String>() {
       public String getValue(Message object) {
         return object.getSender();
       }
-    };
-    table.addColumn(senderColumn, "Sender");
+    });
 
-    TextColumn<Message> subjectColumn = new TextColumn<Message>() {
-      @Override
+    addColumn(table, "Subject", new GetValue<Message, String>() {
       public String getValue(Message object) {
         return object.getSubject();
       }
-    };
-    table.addColumn(subjectColumn, "Subject");
+    });
 
-    SimpleColumn<Message, String> toggleColumn =
-      new SimpleColumn<Message, String>(ButtonCell.getInstance()) {
+    SimpleColumn<Message, String> toggleColumn = new SimpleColumn<Message, String>(
+        ButtonCell.getInstance()) {
       @Override
       public String getValue(Message object) {
         return object.isRead ? "Mark Unread" : "Mark Read";
@@ -395,10 +427,64 @@
     return p;
   }
 
+  private Column<Message, String, Void> addColumn(
+      PagingTableListView<Message> table, final String text,
+      final GetValue<Message, String> getter) {
+    return addColumn(table, text, TextCell.getInstance(), getter, null);
+  }
+
+  private <C extends Comparable<C>> Column<Message, C, Void> addColumn(
+      PagingTableListView<Message> table, final String text,
+      final Cell<C, Void> cell, final GetValue<Message, C> getter,
+      final Comparator<Message> comparator) {
+    Column<Message, C, Void> column = new Column<Message, C, Void>(cell) {
+      @Override
+      public C getValue(Message object) {
+        return getter.getValue(object);
+      }
+    };
+    Header<String> header = new Header<String>(ClickableTextCell.getInstance()) {
+      @Override
+      public String getValue() {
+        return text;
+      }
+    };
+    header.setUpdater(new ValueUpdater<String, Void>() {
+      boolean sortUp = true;
+
+      public void update(String value, Void viewData) {
+        if (comparator == null) {
+          sortMessages(new Comparator<Message>() {
+            public int compare(Message o1, Message o2) {
+              return getter.getValue(o1).compareTo(getter.getValue(o2));
+            }
+          }, sortUp);
+        } else {
+          sortMessages(comparator, sortUp);
+        }
+        sortUp = !sortUp;
+      }
+    });
+    table.addColumn(column, header);
+    return column;
+  }
+
   private Button makeButton(String label, String id) {
     Button button = new Button(label);
     button.getElement().setId(id);
     button.addClickHandler(this);
     return button;
   }
+
+  private void sortMessages(final Comparator<Message> comparator, boolean sortUp) {
+    if (sortUp) {
+      Collections.sort(messages, comparator);
+    } else {
+      Collections.sort(messages, new Comparator<Message>() {
+        public int compare(Message o1, Message o2) {
+          return -comparator.compare(o1, o2);
+        }
+      });
+    }
+  }
 }
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SelectionColumn.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SelectionColumn.java
new file mode 100644
index 0000000..3f71a7e
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SelectionColumn.java
@@ -0,0 +1,52 @@
+/*
+ * 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.cookbook.client;
+
+import com.google.gwt.bikeshed.cells.client.CheckboxCell;
+import com.google.gwt.bikeshed.cells.client.FieldUpdater;
+import com.google.gwt.bikeshed.list.client.SimpleColumn;
+import com.google.gwt.bikeshed.list.shared.SelectionModel;
+
+/**
+ * A column that displays a checkbox that is synchronized with a given
+ * selection model.
+ * 
+ * @param <T> the record data type, used by the row and the selection model
+ */
+public class SelectionColumn<T> extends SimpleColumn<T, Boolean> {
+  
+  private final SelectionModel<T> selectionModel;
+
+  public SelectionColumn(final SelectionModel<T> selectionModel) {
+    super(new CheckboxCell());
+    setFieldUpdater(new FieldUpdater<T, Boolean, Void>() {
+      public void update(int index, T object, Boolean value, Void viewData) {
+        selectionModel.setSelected(object, value);
+      }
+    });
+    this.selectionModel = selectionModel;
+  }
+
+  @Override
+  public boolean dependsOnSelection() {
+    return true;
+  }
+  
+  @Override
+  public Boolean getValue(T object) {
+    return selectionModel.isSelected(object);
+  }
+}
diff --git a/bikeshed/war/Cookbook.css b/bikeshed/war/Cookbook.css
index e63c422..de7f1b6 100644
--- a/bikeshed/war/Cookbook.css
+++ b/bikeshed/war/Cookbook.css
@@ -24,6 +24,10 @@
   background-color: rgb(220, 220, 220);
 }
 
+tr.hover {
+  background-color: rgb(255, 0, 0);
+}
+
 div.gwt-sstree-column {
   overflow-y: scroll;
   overflow-x: auto;