Adding column sorting support to CellTable. Clicking on a header of a sortable Column in CellTable adds a sort icon to the Header and fires a ColumnSortEvent, which user can catch to handle sorting. By default and for backward compatibility, Columns are not sortable. We provide ColumnSortEvent.ListHandler as a default implementation to sort java.util.Lists by mapping Columns to Comparators.

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9493 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactDatabase.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactDatabase.java
index 4d26709..0c06c4c 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactDatabase.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactDatabase.java
@@ -55,8 +55,7 @@
     /**
      * The key provider that provides the unique ID of a contact.
      */
-    public static final ProvidesKey<ContactInfo> KEY_PROVIDER = new ProvidesKey<
-        ContactInfo>() {
+    public static final ProvidesKey<ContactInfo> KEY_PROVIDER = new ProvidesKey<ContactInfo>() {
       public Object getKey(ContactInfo item) {
         return item == null ? null : item.getId();
       }
@@ -78,8 +77,8 @@
     }
 
     public int compareTo(ContactInfo o) {
-      return (o == null || o.firstName == null) ? -1 : -o.firstName.compareTo(
-          firstName);
+      return (o == null || o.firstName == null) ? -1
+          : -o.firstName.compareTo(firstName);
     }
 
     @Override
@@ -315,8 +314,7 @@
   /**
    * The provider that holds the list of contacts in the database.
    */
-  private ListDataProvider<ContactInfo> dataProvider = new ListDataProvider<
-      ContactInfo>();
+  private ListDataProvider<ContactInfo> dataProvider = new ListDataProvider<ContactInfo>();
 
   private final Category[] categories;
 
@@ -371,6 +369,10 @@
     }
   }
 
+  public ListDataProvider<ContactInfo> getDataProvider() {
+    return dataProvider;
+  }
+
   /**
    * Get the categories in the database.
    *
@@ -442,8 +444,8 @@
 
     // Create a birthday between 20-80 years ago.
     int year = (new Date()).getYear() - 21 - Random.nextInt(61);
-    contact.setBirthday(
-        new Date(year, Random.nextInt(12), 1 + Random.nextInt(31)));
+    contact.setBirthday(new Date(year, Random.nextInt(12),
+        1 + Random.nextInt(31)));
 
     // Create an address.
     int addrNum = 1 + Random.nextInt(999);
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 5232d00..c86b630 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
@@ -34,6 +34,7 @@
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.cellview.client.CellTable;
 import com.google.gwt.user.cellview.client.Column;
+import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler;
 import com.google.gwt.user.cellview.client.SimplePager;
 import com.google.gwt.user.cellview.client.SimplePager.TextLocation;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -43,6 +44,7 @@
 import com.google.gwt.view.client.SelectionModel;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 
 /**
@@ -121,6 +123,11 @@
     cellTable = new CellTable<ContactInfo>(
         ContactDatabase.ContactInfo.KEY_PROVIDER);
 
+    // Attach a column sort handler to the ListDataProvider to sort the list.
+    ListHandler<ContactInfo> sortHandler = new ListHandler<ContactInfo>(
+        ContactDatabase.get().getDataProvider().getList());
+    cellTable.addColumnSortHandler(sortHandler);
+
     // Create a Pager to control the table.
     SimplePager.Resources pagerResources = GWT.create(SimplePager.Resources.class);
     pager = new SimplePager(TextLocation.CENTER, pagerResources, false, 0, true);
@@ -130,10 +137,10 @@
     final SelectionModel<ContactInfo> selectionModel = new MultiSelectionModel<ContactInfo>(
         ContactDatabase.ContactInfo.KEY_PROVIDER);
     cellTable.setSelectionModel(selectionModel,
-        DefaultSelectionEventManager.<ContactInfo>createCheckboxManager());
+        DefaultSelectionEventManager.<ContactInfo> createCheckboxManager());
 
     // Initialize the columns.
-    initTableColumns(selectionModel);
+    initTableColumns(selectionModel, sortHandler);
 
     // Add the CellList to the adapter in the database.
     ContactDatabase.get().addDataDisplay(cellTable);
@@ -163,7 +170,9 @@
    * Add the columns to the table.
    */
   @ShowcaseSource
-  private void initTableColumns(final SelectionModel<ContactInfo> selectionModel) {
+  private void initTableColumns(
+      final SelectionModel<ContactInfo> selectionModel,
+      ListHandler<ContactInfo> sortHandler) {
     // Checkbox column. This table will uses a checkbox column for selection.
     // Alternatively, you can call cellTable.setSelectionEnabled(true) to enable
     // mouse selection.
@@ -185,6 +194,12 @@
         return object.getFirstName();
       }
     };
+    firstNameColumn.setSortable(true);
+    sortHandler.setComparator(firstNameColumn, new Comparator<ContactInfo>() {
+      public int compare(ContactInfo o1, ContactInfo o2) {
+        return o1.getFirstName().compareTo(o2.getFirstName());
+      }
+    });
     cellTable.addColumn(firstNameColumn, constants.cwCellTableColumnFirstName());
     firstNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
       public void update(int index, ContactInfo object, String value) {
@@ -202,6 +217,12 @@
         return object.getLastName();
       }
     };
+    lastNameColumn.setSortable(true);
+    sortHandler.setComparator(lastNameColumn, new Comparator<ContactInfo>() {
+      public int compare(ContactInfo o1, ContactInfo o2) {
+        return o1.getLastName().compareTo(o2.getLastName());
+      }
+    });
     cellTable.addColumn(lastNameColumn, constants.cwCellTableColumnLastName());
     lastNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
       public void update(int index, ContactInfo object, String value) {
@@ -238,11 +259,19 @@
     });
 
     // Address.
-    cellTable.addColumn(new Column<ContactInfo, String>(new TextCell()) {
+    Column<ContactInfo, String> addressColumn = new Column<ContactInfo, String>(
+        new TextCell()) {
       @Override
       public String getValue(ContactInfo object) {
         return object.getAddress();
       }
-    }, constants.cwCellTableColumnAddress());
+    };
+    addressColumn.setSortable(true);
+    sortHandler.setComparator(addressColumn, new Comparator<ContactInfo>() {
+      public int compare(ContactInfo o1, ContactInfo o2) {
+        return o1.getAddress().compareTo(o2.getAddress());
+      }
+    });
+    cellTable.addColumn(addressColumn, constants.cwCellTableColumnAddress());
   }
 }
diff --git a/user/src/com/google/gwt/cell/client/IconCellDecorator.java b/user/src/com/google/gwt/cell/client/IconCellDecorator.java
index 8f67fb1..dc00d9b 100644
--- a/user/src/com/google/gwt/cell/client/IconCellDecorator.java
+++ b/user/src/com/google/gwt/cell/client/IconCellDecorator.java
@@ -45,20 +45,20 @@
     /**
      * The wrapper around the image vertically aligned to the bottom.
      */
-    @Template("<div style=\"position:absolute;{0}:0px;bottom:0px;\">{1}</div>")
+    @Template("<div style=\"position:absolute;{0}:0px;bottom:0px;line-height:0px;\">{1}</div>")
     SafeHtml imageWrapperBottom(String direction, SafeHtml image);
 
     /**
      * The wrapper around the image vertically aligned to the middle.
      */
-    @Template("<div style=\"position:absolute;{0}:0px;top:50%;"
+    @Template("<div style=\"position:absolute;{0}:0px;top:50%;line-height:0px;"
         + "margin-top:-{1}px;\">{2}</div>")
     SafeHtml imageWrapperMiddle(String direction, int halfHeight, SafeHtml image);
 
     /**
      * The wrapper around the image vertically aligned to the top.
      */
-    @Template("<div style=\"position:absolute;{0}:0px;top:0px;\">{1}</div>")
+    @Template("<div style=\"position:absolute;{0}:0px;top:0px;line-height:0px;\">{1}</div>")
     SafeHtml imageWrapperTop(String direction, SafeHtml image);
   }
 
@@ -196,8 +196,7 @@
     } else if (HasVerticalAlignment.ALIGN_BOTTOM == valign) {
       return template.imageWrapperBottom(direction, image);
     } else {
-      // Add one to the margin-top because it looks better in all browsers.
-      int halfHeight = 1 + (int) Math.round(res.getHeight() / 2.0);
+      int halfHeight = (int) Math.round(res.getHeight() / 2.0);
       return template.imageWrapperMiddle(direction, halfHeight, image);
     }
   }
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 36cab91..71b07aa 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTable.css
+++ b/user/src/com/google/gwt/user/cellview/client/CellTable.css
@@ -62,6 +62,23 @@
   
 }
 
+.cellTableSortableHeader {
+  cursor: pointer;
+  cursor: hand;
+}
+
+.cellTableSortableHeader:hover {
+  color: #6c6b6b;
+}
+
+.cellTableSortedHeaderAscending {
+
+}
+
+.cellTableSortedHeaderDescending {
+
+}
+
 .cellTableEvenRow {
   background: #ffffff;
 }
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 65ec3c2..65330ab 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTable.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTable.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.cell.client.Cell;
 import com.google.gwt.cell.client.Cell.Context;
+import com.google.gwt.cell.client.IconCellDecorator;
+import com.google.gwt.cell.client.SafeHtmlCell;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.dom.client.Document;
@@ -30,6 +32,7 @@
 import com.google.gwt.dom.client.TableRowElement;
 import com.google.gwt.dom.client.TableSectionElement;
 import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.CssResource;
@@ -41,6 +44,7 @@
 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.ColumnSortList.ColumnSortInfo;
 import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
@@ -59,21 +63,19 @@
  * A tabular view that supports paging and columns.
  * 
  * <p>
- * <h3>Columns</h3>
- * The {@link Column} class defines the {@link Cell} used to render a column.
- * Implement {@link Column#getValue(Object)} to retrieve the field value from
- * the row object that will be rendered in the {@link Cell}.
+ * <h3>Columns</h3> The {@link Column} class defines the {@link Cell} used to
+ * render a column. Implement {@link Column#getValue(Object)} to retrieve the
+ * field value from the row object that will be rendered in the {@link Cell}.
  * </p>
  * 
  * <p>
- * <h3>Headers and Footers</h3>
- * A {@link Header} can be placed at the top (header) or bottom (footer) of the
- * {@link CellTable}. You can specify a header as text using
- * {@link #addColumn(Column, String)}, or you can create a custom {@link Header}
- * that can change with the value of the cells, such as a column total. The
- * {@link Header} will be rendered every time the row data changes or the table
- * is redrawn. If you pass the same header instance (==) into adjacent columns,
- * the header will span the columns.
+ * <h3>Headers and Footers</h3> A {@link Header} can be placed at the top
+ * (header) or bottom (footer) of the {@link CellTable}. You can specify a
+ * header as text using {@link #addColumn(Column, String)}, or you can create a
+ * custom {@link Header} that can change with the value of the cells, such as a
+ * column total. The {@link Header} will be rendered every time the row data
+ * changes or the table is redrawn. If you pass the same header instance (==)
+ * into adjacent columns, the header will span the columns.
  * </p>
  * 
  * <p>
@@ -134,6 +136,20 @@
     ImageResource cellTableSelectedBackground();
 
     /**
+     * Icon used when a column is sorted in ascending order.
+     */
+    @Source("sortAscending.png")
+    @ImageOptions(flipRtl = true)
+    ImageResource cellTableSortAscending();
+
+    /**
+     * Icon used when a column is sorted in descending order.
+     */
+    @Source("sortDescending.png")
+    @ImageOptions(flipRtl = true)
+    ImageResource cellTableSortDescending();
+
+    /**
      * The styles used in this widget.
      */
     @Source(Style.DEFAULT_CSS)
@@ -256,6 +272,21 @@
     String cellTableSelectedRowCell();
 
     /**
+     * Applied to header cells that are sortable.
+     */
+    String cellTableSortableHeader();
+
+    /**
+     * Applied to header cells that are sorted in ascending order.
+     */
+    String cellTableSortedHeaderAscending();
+
+    /**
+     * Applied to header cells that are sorted in descending order.
+     */
+    String cellTableSortedHeaderDescending();
+
+    /**
      * Applied to the table.
      */
     String cellTableWidget();
@@ -476,15 +507,18 @@
 
   private int keyboardSelectedColumn = 0;
 
+  private final Resources resources;
   private RowStyles<T> rowStyles;
+  private IconCellDecorator<SafeHtml> sortAscDecorator;
+  private IconCellDecorator<SafeHtml> sortDescDecorator;
+  private final ColumnSortList sortList;
   private final Style style;
   private final TableElement table;
   private final TableSectionElement tbody;
   private final TableSectionElement tbodyLoading;
-
   private final TableSectionElement tfoot;
-
   private final TableSectionElement thead;
+  private boolean updatingSortList;
 
   /**
    * Constructs a table with a default page size of 15.
@@ -554,9 +588,19 @@
     if (template == null) {
       template = GWT.create(Template.class);
     }
+    this.resources = resources;
     this.style = resources.cellTableStyle();
     this.style.ensureInjected();
 
+    // Create the ColumnSortList and delegate.
+    sortList = new ColumnSortList(new ColumnSortList.Delegate() {
+      public void onModification() {
+        if (!updatingSortList) {
+          createHeaders(false);
+        }
+      }
+    });
+
     table = getElement().cast();
     table.setCellSpacing(0);
     colgroup = Document.get().createColGroupElement();
@@ -669,6 +713,17 @@
   }
 
   /**
+   * Add a handler to handle {@link ColumnSortEvent}s.
+   * 
+   * @param handler the {@link ColumnSortEvent.Handler} to add
+   * @return a {@link HandlerRegistration} to remove the handler
+   */
+  public HandlerRegistration addColumnSortHandler(
+      ColumnSortEvent.Handler handler) {
+    return addHandler(handler, ColumnSortEvent.getType());
+  }
+
+  /**
    * Add a style name to the {@link TableColElement} at the specified index,
    * creating it if necessary.
    * 
@@ -710,6 +765,27 @@
   }
 
   /**
+   * Get the index of the specified column.
+   * 
+   * @param column the column to search for
+   * @return the index of the column, or -1 if not found
+   */
+  public int getColumnIndex(Column<T,?> column) {
+    return columns.indexOf(column);
+  }
+
+  /**
+   * Get the {@link ColumnSortList} that specifies which columns are sorted.
+   * Modifications to the {@link ColumnSortList} will be reflected in the table
+   * header.
+   * 
+   * @return the {@link ColumnSortList}
+   */
+  public ColumnSortList getColumnSortList() {
+    return sortList;
+  }
+
+  /**
    * Return the height of the table header.
    * 
    * @return an int representing the header height
@@ -1054,12 +1130,25 @@
     TableSectionElement section = TableSectionElement.as(sectionElem);
 
     // Forward the event to the associated header, footer, or column.
+    boolean isClick = "click".equals(eventType);
     int col = tableCell.getCellIndex();
     if (section == thead) {
       Header<?> header = headers.get(col);
-      if (header != null && cellConsumesEventType(header.getCell(), eventType)) {
-        Context context = new Context(0, col, header.getKey());
-        header.onBrowserEvent(context, tableCell, event);
+      if (header != null) {
+        // Fire the event to the header.
+        if (cellConsumesEventType(header.getCell(), eventType)) {
+          Context context = new Context(0, col, header.getKey());
+          header.onBrowserEvent(context, tableCell, event);
+        }
+
+        // Sort the header.
+        Column<T, ?> column = columns.get(col);
+        if (isClick && column.isSortable()) {
+          updatingSortList = true;
+          sortList.push(column);
+          updatingSortList = false;
+          ColumnSortEvent.fire(this, sortList);
+        }
       }
     } else if (section == tfoot) {
       Header<?> footer = footers.get(col);
@@ -1069,7 +1158,6 @@
       }
     } else if (section == tbody) {
       // Update the hover state.
-      boolean isClick = "click".equals(eventType);
       int row = tr.getSectionRowIndex();
       if ("mouseover".equals(eventType)) {
         // Unstyle the old row if it is still part of the table.
@@ -1339,19 +1427,41 @@
     TableSectionElement section = isFooter ? tfoot : thead;
     String className = isFooter ? style.cellTableFooter()
         : style.cellTableHeader();
+    String firstColumnStyle = " "
+        + (isFooter ? style.cellTableFirstColumnFooter()
+            : style.cellTableFirstColumnHeader());
+    String lastColumnStyle = " "
+        + (isFooter ? style.cellTableLastColumnFooter()
+            : style.cellTableLastColumnHeader());
+    String sortableStyle = " " + style.cellTableSortableHeader();
+    String sortedAscStyle = " " + style.cellTableSortedHeaderAscending();
+    String sortedDescStyle = " " + style.cellTableSortedHeaderDescending();
 
     boolean hasHeader = false;
     SafeHtmlBuilder sb = new SafeHtmlBuilder();
     sb.appendHtmlConstant("<tr>");
     int columnCount = columns.size();
     if (columnCount > 0) {
+      // Get information about the sorted column.
+      ColumnSortInfo sortedInfo = (sortList.size() == 0) ? null
+          : sortList.get(0);
+      Column<?, ?> sortedColumn = (sortedInfo == null) ? null
+          : sortedInfo.getColumn();
+      boolean isSortAscending = (sortedInfo == null) ? false
+          : sortedInfo.isAscending();
+
       // Setup the first column.
       Header<?> prevHeader = theHeaders.get(0);
+      Column<T, ?> column = columns.get(0);
       int prevColspan = 1;
+      boolean isSortable = false;
+      boolean isSorted = false;
       StringBuilder classesBuilder = new StringBuilder(className);
-      classesBuilder.append(" ");
-      classesBuilder.append(isFooter ? style.cellTableFirstColumnFooter()
-          : style.cellTableFirstColumnHeader());
+      classesBuilder.append(firstColumnStyle);
+      if (!isFooter && column.isSortable()) {
+        isSortable = true;
+        isSorted = (column == sortedColumn);
+      }
 
       // Loop through all column headers.
       int curColumn;
@@ -1360,41 +1470,86 @@
 
         if (header != prevHeader) {
           // The header has changed, so append the previous one.
-          SafeHtmlBuilder headerBuilder = new SafeHtmlBuilder();
+          SafeHtml headerHtml = SafeHtmlUtils.EMPTY_SAFE_HTML;
           if (prevHeader != null) {
             hasHeader = true;
+
+            // Build the header.
+            SafeHtmlBuilder headerBuilder = new SafeHtmlBuilder();
             Context context = new Context(0, curColumn - prevColspan,
                 prevHeader.getKey());
             prevHeader.render(context, headerBuilder);
+
+            // Wrap the header with a sort icon.
+            if (isSorted) {
+              SafeHtml unwrappedHeader = headerBuilder.toSafeHtml();
+              headerBuilder = new SafeHtmlBuilder();
+              getSortDecorator(isSortAscending).render(null, unwrappedHeader,
+                  headerBuilder);
+            }
+            headerHtml = headerBuilder.toSafeHtml();
+          }
+          if (isSortable) {
+            classesBuilder.append(sortableStyle);
+          }
+          if (isSorted) {
+            classesBuilder.append(isSortAscending ? sortedAscStyle
+                : sortedDescStyle);
           }
           sb.append(template.th(prevColspan, classesBuilder.toString(),
-              headerBuilder.toSafeHtml()));
+              headerHtml));
 
           // Reset the previous header.
           prevHeader = header;
           prevColspan = 1;
           classesBuilder = new StringBuilder(className);
+          isSortable = false;
+          isSorted = false;
         } else {
           // Increment the colspan if the headers == each other.
           prevColspan++;
         }
+
+        // Update the sorted state.
+        column = columns.get(curColumn);
+        if (!isFooter && column.isSortable()) {
+          isSortable = true;
+          isSorted = (column == sortedColumn);
+        }
       }
 
       // Append the last header.
-      SafeHtmlBuilder headerBuilder = new SafeHtmlBuilder();
+      SafeHtml headerHtml = SafeHtmlUtils.EMPTY_SAFE_HTML;
       if (prevHeader != null) {
         hasHeader = true;
+
+        // Build the header.
+        SafeHtmlBuilder headerBuilder = new SafeHtmlBuilder();
         Context context = new Context(0, curColumn - prevColspan,
             prevHeader.getKey());
         prevHeader.render(context, headerBuilder);
+
+        // Wrap the header with a sort icon.
+        if (isSorted) {
+          SafeHtml unwrappedHeader = headerBuilder.toSafeHtml();
+          headerBuilder = new SafeHtmlBuilder();
+          getSortDecorator(isSortAscending).render(null, unwrappedHeader,
+              headerBuilder);
+        }
+        headerHtml = headerBuilder.toSafeHtml();
+      }
+      if (isSortable) {
+        classesBuilder.append(sortableStyle);
+      }
+      if (isSorted) {
+        classesBuilder.append(isSortAscending ? sortedAscStyle
+            : sortedDescStyle);
       }
 
       // The first and last columns could be the same column.
       classesBuilder.append(" ");
-      classesBuilder.append(isFooter ? style.cellTableLastColumnFooter()
-          : style.cellTableLastColumnHeader());
-      sb.append(template.th(prevColspan, classesBuilder.toString(),
-          headerBuilder.toSafeHtml()));
+      classesBuilder.append(lastColumnStyle);
+      sb.append(template.th(prevColspan, classesBuilder.toString(), headerHtml));
     }
     sb.appendHtmlConstant("</tr>");
 
@@ -1522,6 +1677,28 @@
     return element.clientHeight;
   }-*/;
 
+  /**
+   * Get the {@link IconCellDecorator} used to decorate sorted column headers.
+   * 
+   * @param ascending true if ascending, false if descending
+   * @return the {@link IconCellDecorator}
+   */
+  private IconCellDecorator<SafeHtml> getSortDecorator(boolean ascending) {
+    if (ascending) {
+      if (sortAscDecorator == null) {
+        sortAscDecorator = new IconCellDecorator<SafeHtml>(
+            resources.cellTableSortAscending(), new SafeHtmlCell());
+      }
+      return sortAscDecorator;
+    } else {
+      if (sortDescDecorator == null) {
+        sortDescDecorator = new IconCellDecorator<SafeHtml>(
+            resources.cellTableSortDescending(), new SafeHtmlCell());
+      }
+      return sortDescDecorator;
+    }
+  }
+
   private boolean handleKey(Event event) {
     HasDataPresenter<T> presenter = getPresenter();
     int oldRow = getKeyboardSelectedRow();
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 bca708b..f143f6d 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTableBasic.css
+++ b/user/src/com/google/gwt/user/cellview/client/CellTableBasic.css
@@ -69,6 +69,23 @@
   
 }
 
+.cellTableSortableHeader {
+  cursor: pointer;
+  cursor: hand;
+}
+
+.cellTableSortableHeader:hover {
+  color: #6c6b6b;
+}
+
+.cellTableSortedHeaderAscending {
+
+}
+
+.cellTableSortedHeaderDescending {
+
+}
+
 .cellTableEvenRow {
   background: #ffffff;
 }
diff --git a/user/src/com/google/gwt/user/cellview/client/Column.java b/user/src/com/google/gwt/user/cellview/client/Column.java
index f74f5da..5a6f96c 100644
--- a/user/src/com/google/gwt/user/cellview/client/Column.java
+++ b/user/src/com/google/gwt/user/cellview/client/Column.java
@@ -46,6 +46,7 @@
    */
   private FieldUpdater<T, C> fieldUpdater;
 
+  private boolean isSortable = false;
   private HorizontalAlignmentConstant hAlign = null;
   private VerticalAlignmentConstant vAlign = null;
 
@@ -91,6 +92,15 @@
   }
 
   /**
+   * Check if the column is sortable.
+   * 
+   * @return true if sortable, false if not
+   */
+  public boolean isSortable() {
+    return isSortable;
+  }
+
+  /**
    * Handle a browser event that took place within the column.
    * 
    * @param context the cell context
@@ -144,6 +154,16 @@
   }
 
   /**
+   * Set whether or not the column can be sorted. The change will take effect
+   * the next time the table is redrawn.
+   * 
+   * @param sortable true to make sortable, false to make unsortable
+   */
+  public void setSortable(boolean sortable) {
+    this.isSortable = sortable;
+  }
+
+  /**
    * {@inheritDoc}
    * 
    * <p>
diff --git a/user/src/com/google/gwt/user/cellview/client/ColumnSortEvent.java b/user/src/com/google/gwt/user/cellview/client/ColumnSortEvent.java
new file mode 100644
index 0000000..7e5c524
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/ColumnSortEvent.java
@@ -0,0 +1,191 @@
+/*
+ * 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.user.cellview.client;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HasHandlers;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a column sort event.
+ */
+public class ColumnSortEvent extends GwtEvent<ColumnSortEvent.Handler> {
+
+  /**
+   * Handler for {@link ColumnSortEvent}.
+   */
+  public static interface Handler extends EventHandler {
+
+    /**
+     * Called when {@link ColumnSortEvent} is fired.
+     * 
+     * @param event the {@link ColumnSortEvent} that was fired
+     */
+    void onColumnSort(ColumnSortEvent event);
+  }
+
+  /**
+   * <p>
+   * A default handler used to sort a {@link List} backing a table. If the
+   * sorted column has an associated {@link Comparator}, the list is sorted
+   * using the comparator.
+   * </p>
+   * 
+   * <p>
+   * This can be used in conjunction with
+   * {@link com.google.gwt.view.client.ListDataProvider}.
+   * </p>
+   * 
+   * @param <T> the data type of the list
+   */
+  public static class ListHandler<T> implements Handler {
+    private final Map<Column<?, ?>, Comparator<T>> comparators = new HashMap<Column<?, ?>, Comparator<T>>();
+    private final List<T> list;
+
+    public ListHandler(List<T> list) {
+      this.list = list;
+    }
+
+    public List<T> getList() {
+      return list;
+    }
+
+    public void onColumnSort(ColumnSortEvent event) {
+      // Get the sorted column.
+      Column<?, ?> column = event.getColumn();
+      if (column == null) {
+        return;
+      }
+
+      // Get the comparator.
+      final Comparator<T> comparator = comparators.get(column);
+      if (comparator == null) {
+        return;
+      }
+
+      // Sort using the comparator.
+      if (event.isSortAcsending()) {
+        Collections.sort(list, comparator);
+      } else {
+        Collections.sort(list, new Comparator<T>() {
+          public int compare(T o1, T o2) {
+            return -comparator.compare(o1, o2);
+          }
+        });
+      }
+    }
+
+    /**
+     * Set the comparator used to sort the specified column in ascending order.
+     * 
+     * @param column the {@link Column}
+     * @param comparator the {@link Comparator} to use for the {@link Column}
+     */
+    public void setComparator(Column<T, ?> column, Comparator<T> comparator) {
+      comparators.put(column, comparator);
+    }
+  }
+
+  /**
+   * Handler type.
+   */
+  private static Type<Handler> TYPE;
+
+  /**
+   * Fires a cell preview event on all registered handlers in the handler
+   * manager. If no such handlers exist, this implementation will do nothing.
+   * 
+   * @param source the source of the event
+   * @param sortList the {@link ColumnSortList} of sorted columns
+   * @return the {@link ColumnSortEvent} that was fired
+   */
+  public static ColumnSortEvent fire(HasHandlers source, ColumnSortList sortList) {
+    ColumnSortEvent event = new ColumnSortEvent(sortList);
+    if (TYPE != null) {
+      source.fireEvent(event);
+    }
+    return event;
+  }
+
+  /**
+   * Gets the type associated with this event.
+   * 
+   * @return returns the handler type
+   */
+  public static Type<Handler> getType() {
+    if (TYPE == null) {
+      TYPE = new Type<Handler>();
+    }
+    return TYPE;
+  }
+
+  private final ColumnSortList sortList;
+
+  /**
+   * Construct a new {@link ColumnSortEvent}.
+   * 
+   * @param sortList the {@link ColumnSortList}
+   */
+  protected ColumnSortEvent(ColumnSortList sortList) {
+    this.sortList = sortList;
+  }
+
+  @Override
+  public Type<Handler> getAssociatedType() {
+    return TYPE;
+  }
+
+  /**
+   * Get the {@link Column} that was sorted.
+   * 
+   * @return the sorted {@link Column}, or null if not sorted
+   */
+  public Column<?, ?> getColumn() {
+    return (sortList == null || sortList.size() == 0) ? null
+        : sortList.get(0).getColumn();
+  }
+
+  /**
+   * Get the {@link ColumnSortList} that contains the ordered list of sorted
+   * columns.
+   * 
+   * @return the {@link ColumnSortList}
+   */
+  public ColumnSortList getColumnSortList() {
+    return sortList;
+  }
+
+  /**
+   * Check if the {@link Column} is sorted in ascending order.
+   * 
+   * @return true if ascending, false if descending or not sorted
+   */
+  public boolean isSortAcsending() {
+    return (sortList == null || sortList.size() == 0) ? false
+        : sortList.get(0).isAscending();
+  }
+
+  @Override
+  protected void dispatch(Handler handler) {
+    handler.onColumnSort(this);
+  }
+}
diff --git a/user/src/com/google/gwt/user/cellview/client/ColumnSortList.java b/user/src/com/google/gwt/user/cellview/client/ColumnSortList.java
new file mode 100644
index 0000000..e2c2985
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/ColumnSortList.java
@@ -0,0 +1,270 @@
+/*
+ * 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.user.cellview.client;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An ordered list containing the sort history of {@link Column}s in a table.
+ * The 0th item is the {@link ColumnSortInfo} of the most recently sorted
+ * column.
+ */
+public class ColumnSortList {
+
+  /**
+   * Information about the sort order of a specific column in a table.
+   */
+  public static class ColumnSortInfo {
+
+    private final boolean ascending;
+    private final Column<?, ?> column;
+
+    /**
+     * Construct a new {@link ColumnSortInfo}.
+     * 
+     * @param column the column index
+     * @param ascending true if sorted ascending
+     */
+    public ColumnSortInfo(Column<?, ?> column, boolean ascending) {
+      this.column = column;
+      this.ascending = ascending;
+    }
+
+    /**
+     * Default constructor used for RPC.
+     */
+    ColumnSortInfo() {
+      this(null, true);
+    }
+
+    /**
+     * Check if this object is equal to another. The objects are equal if the
+     * column and ascending values are the equal.
+     * 
+     * @param obj the object to check for equality
+     * @return true if objects are the same
+     */
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      } else if (!(obj instanceof ColumnSortInfo)) {
+        return false;
+      }
+
+      ColumnSortInfo other = (ColumnSortInfo) obj;
+      return equalsOrBothNull(getColumn(), other.getColumn())
+          && isAscending() == other.isAscending();
+    }
+
+    /**
+     * Get the {@link Column} that was sorted.
+     * 
+     * @return the {@link Column}
+     */
+    public Column<?, ?> getColumn() {
+      return column;
+    }
+
+    @Override
+    public int hashCode() {
+      return 31 * (column == null ? 0 : column.hashCode())
+          + (ascending ? 1 : 0);
+    }
+
+    /**
+     * Check if the column was sorted in ascending or descending order.
+     * 
+     * @return true if ascending, false if descending
+     */
+    public boolean isAscending() {
+      return ascending;
+    }
+
+    private boolean equalsOrBothNull(Object a, Object b) {
+      return a == null ? b == null : a.equals(b);
+    }
+  }
+
+  /**
+   * The delegate that handles modifications to the list.
+   */
+  public static interface Delegate {
+
+    /**
+     * Called when the list is modified.
+     */
+    void onModification();
+  }
+
+  /**
+   * The delegate that handles modifications.
+   */
+  private final Delegate delegate;
+
+  /**
+   * A List used to manage the insertion/removal of {@link ColumnSortInfo}.
+   */
+  private final List<ColumnSortInfo> infos = new ArrayList<ColumnSortInfo>();
+
+  /**
+   * Construct a new {@link ColumnSortList} without a {@link Delegate}.
+   */
+  public ColumnSortList() {
+    this(null);
+  }
+
+  /**
+   * Construct a new {@link ColumnSortList} with the specified {@link Delegate}.
+   * 
+   * @param delegate the {@link Delegate} to inform of modifications
+   */
+  public ColumnSortList(Delegate delegate) {
+    this.delegate = delegate;
+  }
+
+  /**
+   * Removes all of the elements from this list.
+   */
+  public void clear() {
+    infos.clear();
+    fireDelegate();
+  }
+
+  /**
+   * Check if the specified object equals this list. Two {@link ColumnSortList}
+   * are equals if they are the same size, and all entries are
+   * <code>equals</code> and in the same order.
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    } else if (!(obj instanceof ColumnSortList)) {
+      return false;
+    }
+
+    // Check the size of the lists.
+    ColumnSortList other = (ColumnSortList) obj;
+    return infos.equals(other.infos);
+  }
+
+  /**
+   * Get the {@link ColumnSortInfo} at the specified index.
+   * 
+   * @param index the index
+   * @return the {@link ColumnSortInfo}
+   */
+  public ColumnSortInfo get(int index) {
+    return infos.get(index);
+  }
+
+  @Override
+  public int hashCode() {
+    return 31 * infos.hashCode() + 13;
+  }
+
+  /**
+   * Inserts the specified {@link ColumnSortInfo} at the specified position in
+   * this list. If the column already exists in the sort info, the index will be
+   * adjusted to account for any removed entries.
+   * 
+   * @param sortInfo the {@link ColumnSortInfo} to add
+   */
+  public void insert(int index, ColumnSortInfo sortInfo) {
+    if (sortInfo == null) {
+      throw new IllegalArgumentException("sortInfo cannot be null");
+    }
+
+    // Remove sort info for duplicate columns
+    Column<?, ?> column = sortInfo.getColumn();
+    for (int i = 0; i < infos.size(); i++) {
+      ColumnSortInfo curInfo = infos.get(i);
+      if (curInfo.getColumn() == column) {
+        infos.remove(i);
+        if (i < index) {
+          index--;
+        }
+        i--;
+      }
+    }
+
+    // Insert the new sort info
+    infos.add(index, sortInfo);
+    fireDelegate();
+  }
+
+  /**
+   * Push a {@link Column} onto the list at index zero, setting ascending to
+   * true. If the column already exists, it will be removed from its current
+   * position and placed at the start of the list. If the Column is already at
+   * the start of the list, its ascending bit will be flipped (ascending to
+   * descending and vice versa).
+   * 
+   * @param column the {@link Column} to push
+   * @return the {@link ColumnSortInfo} that was pushed
+   */
+  public ColumnSortInfo push(Column<?, ?> column) {
+    // If the column matches the primary column, toggle the order.
+    boolean ascending = true;
+    if (size() > 0 && get(0).getColumn() == column) {
+      ascending = !get(0).isAscending();
+    }
+
+    // Push the new column.
+    ColumnSortInfo toRet = new ColumnSortInfo(column, ascending);
+    push(toRet);
+    return toRet;
+  }
+
+  /**
+   * Push a {@link ColumnSortInfo} onto the list at index zero. If the column
+   * already exists, it will be removed from its current position and placed at
+   * the start of the list.
+   * 
+   * @param sortInfo the {@link ColumnSortInfo} to push
+   */
+  public void push(ColumnSortInfo sortInfo) {
+    insert(0, sortInfo);
+  }
+
+  /**
+   * Remove a {@link ColumnSortInfo} from the list.
+   * 
+   * @param sortInfo the {@link ColumnSortInfo} to remove
+   */
+  public boolean remove(ColumnSortInfo sortInfo) {
+    boolean toRet = infos.remove(sortInfo);
+    fireDelegate();
+    return toRet;
+  }
+
+  /**
+   * Get the size of the list.
+   * 
+   * @return the number of {@link ColumnSortInfo} in the list
+   */
+  public int size() {
+    return infos.size();
+  }
+
+  private void fireDelegate() {
+    if (delegate != null) {
+      delegate.onModification();
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/user/cellview/client/sortAscending.png b/user/src/com/google/gwt/user/cellview/client/sortAscending.png
new file mode 100644
index 0000000..816198f
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/sortAscending.png
Binary files differ
diff --git a/user/src/com/google/gwt/user/cellview/client/sortDescending.png b/user/src/com/google/gwt/user/cellview/client/sortDescending.png
new file mode 100644
index 0000000..05b34f3
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/sortDescending.png
Binary files differ
diff --git a/user/test/com/google/gwt/user/cellview/CellViewSuite.java b/user/test/com/google/gwt/user/cellview/CellViewSuite.java
index e2e8370..3dd2f6e 100644
--- a/user/test/com/google/gwt/user/cellview/CellViewSuite.java
+++ b/user/test/com/google/gwt/user/cellview/CellViewSuite.java
@@ -23,6 +23,8 @@
 import com.google.gwt.user.cellview.client.CellTableTest;
 import com.google.gwt.user.cellview.client.CellTreeTest;
 import com.google.gwt.user.cellview.client.CellWidgetTest;
+import com.google.gwt.user.cellview.client.ColumnSortInfoTest;
+import com.google.gwt.user.cellview.client.ColumnSortListTest;
 import com.google.gwt.user.cellview.client.ColumnTest;
 import com.google.gwt.user.cellview.client.HasDataPresenterTest;
 import com.google.gwt.user.cellview.client.PageSizePagerTest;
@@ -44,6 +46,8 @@
     suite.addTestSuite(CellTableTest.class);
     suite.addTestSuite(CellTreeTest.class);
     suite.addTestSuite(CellWidgetTest.class);
+    suite.addTestSuite(ColumnSortInfoTest.class);
+    suite.addTestSuite(ColumnSortListTest.class);
     suite.addTestSuite(ColumnTest.class);
     suite.addTestSuite(HasDataPresenterTest.class);
     suite.addTestSuite(PageSizePagerTest.class);
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 6d3115c..6585f0e 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellTableTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellTableTest.java
@@ -162,6 +162,26 @@
     RootPanel.get().remove(table);
   }
 
+  public void testGetColumnIndex() {
+    CellTable<String> table = new CellTable<String>();
+    Column<String, String> col0 = new IdentityColumn<String>(new TextCell());
+    table.addColumn(col0);
+    Column<String, String> col1 = new IdentityColumn<String>(new TextCell());
+    table.addColumn(col1);
+    Column<String, String> col2 = new IdentityColumn<String>(new TextCell());
+    table.addColumn(col2);
+    assertEquals(0, table.getColumnIndex(col0));
+    assertEquals(1, table.getColumnIndex(col1));
+    assertEquals(2, table.getColumnIndex(col2));
+
+    // Test a column that is not in the table.
+    Column<String, String> other = new IdentityColumn<String>(new TextCell());
+    assertEquals(-1, table.getColumnIndex(other));
+
+    // Test null.
+    assertEquals(-1, table.getColumnIndex(null));
+  }
+
   public void testGetColumnOutOfBounds() {
     CellTable<String> table = new CellTable<String>();
 
@@ -363,6 +383,54 @@
         styleLastColumn));
   }
 
+  public void testSortableColumn() {
+    CellTable<String> table = createAbstractHasData(new TextCell());
+    table.getColumn(0).setSortable(true);
+    table.getPresenter().flush();
+    RootPanel.get().add(table);
+
+    // Add a column sort handler.
+    final List<Column<?, ?>> lastSorted = new ArrayList<Column<?, ?>>();
+    table.addColumnSortHandler(new ColumnSortEvent.Handler() {
+      public void onColumnSort(ColumnSortEvent event) {
+        lastSorted.clear();
+        lastSorted.add(event.getColumn());
+      }
+    });
+
+    // Default sort order is empty.
+    ColumnSortList sortList = table.getColumnSortList();
+    assertEquals(0, sortList.size());
+
+    // Sort a column that is sortable.
+    NativeEvent click = Document.get().createClickEvent(0, 0, 0, 0, 0, false,
+        false, false, false);
+    getHeaderElement(table, 0).dispatchEvent(click);
+    assertEquals(1, sortList.size());
+    assertEquals(table.getColumn(0), sortList.get(0).getColumn());
+    assertTrue(sortList.get(0).isAscending());
+    assertEquals(1, lastSorted.size());
+    lastSorted.clear();
+
+    // Sort the same column again.
+    getHeaderElement(table, 0).dispatchEvent(click);
+    assertEquals(1, sortList.size());
+    assertEquals(table.getColumn(0), sortList.get(0).getColumn());
+    assertFalse(sortList.get(0).isAscending());
+    assertEquals(1, lastSorted.size());
+    lastSorted.clear();
+
+    // Sort a column that is not sortable.
+    getHeaderElement(table, 1).dispatchEvent(click);
+    assertEquals(1, sortList.size());
+    assertEquals(table.getColumn(0), sortList.get(0).getColumn());
+    assertFalse(sortList.get(0).isAscending());
+    assertEquals(0, lastSorted.size());
+
+    // Cleanup.
+    RootPanel.get().remove(table);
+  }
+
   @Override
   protected CellTable<String> createAbstractHasData(Cell<String> cell) {
     CellTable<String> table = new CellTable<String>();
@@ -371,13 +439,13 @@
       public String getValue(String object) {
         return object;
       }
-    });
+    }, "Column 0");
     table.addColumn(new Column<String, String>(new TextCell()) {
       @Override
       public String getValue(String object) {
         return object + "-2";
       }
-    });
+    }, "Column 1");
     return table;
   }
 
diff --git a/user/test/com/google/gwt/user/cellview/client/ColumnSortInfoTest.java b/user/test/com/google/gwt/user/cellview/client/ColumnSortInfoTest.java
new file mode 100644
index 0000000..bc4f49d
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/ColumnSortInfoTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.user.cellview.client;
+
+import com.google.gwt.cell.client.TextCell;
+import com.google.gwt.user.cellview.client.ColumnSortList.ColumnSortInfo;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link ColumnSortInfo}.
+ */
+public class ColumnSortInfoTest extends TestCase {
+
+  public void testAccessors() {
+    Column<String, String> column = new IdentityColumn<String>(new TextCell());
+    ColumnSortInfo info = new ColumnSortInfo(column, true);
+    assertEquals(column, info.getColumn());
+    assertTrue(info.isAscending());
+  }
+
+  public void testEquals() {
+    // Test equals.
+    Column<String, String> column0 = new IdentityColumn<String>(new TextCell());
+    ColumnSortInfo info0a = new ColumnSortInfo(column0, true);
+    ColumnSortInfo info0b = new ColumnSortInfo(column0, true);
+    assertTrue(info0a.equals(info0b));
+    assertTrue(info0b.equals(info0a));
+    assertEquals(info0a.hashCode(), info0b.hashCode());
+
+    // Test null.
+    assertFalse(info0a.equals(null));
+
+    // Test different object.
+    assertFalse(info0a.equals("not a ColumnSortInfo"));
+
+    // Test different sort order.
+    ColumnSortInfo info0desc = new ColumnSortInfo(column0, false);
+    assertFalse(info0a.equals(info0desc));
+    assertFalse(info0desc.equals(info0a));
+    assertTrue(info0a.hashCode() != info0desc.hashCode());
+
+    // Test different column.
+    Column<String, String> column1 = new IdentityColumn<String>(new TextCell());
+    ColumnSortInfo info1 = new ColumnSortInfo(column1, true);
+    assertFalse(info0a.equals(info1));
+    assertFalse(info1.equals(info0a));
+    assertTrue(info0a.hashCode() != info1.hashCode());
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/user/cellview/client/ColumnSortListTest.java b/user/test/com/google/gwt/user/cellview/client/ColumnSortListTest.java
new file mode 100644
index 0000000..57f30df
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/ColumnSortListTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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.user.cellview.client;
+
+import com.google.gwt.cell.client.TextCell;
+import com.google.gwt.user.cellview.client.ColumnSortList.ColumnSortInfo;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link ColumnSortList}.
+ */
+public class ColumnSortListTest extends TestCase {
+
+  public void testClear() {
+    ColumnSortList list = new ColumnSortList();
+    assertEquals(0, list.size());
+
+    list.push(createColumnSortInfo());
+    list.push(createColumnSortInfo());
+    assertEquals(2, list.size());
+    list.clear();
+    assertEquals(0, list.size());
+  }
+
+  public void testEquals() {
+    ColumnSortList list0 = new ColumnSortList();
+    ColumnSortList list1 = new ColumnSortList();
+
+    // Compare empty lists.
+    assertTrue(list0.equals(list1));
+    assertTrue(list1.equals(list0));
+    assertEquals(list0.hashCode(), list1.hashCode());
+
+    // Compare with one item.
+    ColumnSortInfo info0 = createColumnSortInfo();
+    list0.push(info0);
+    list1.push(info0);
+    assertTrue(list0.equals(list1));
+    assertTrue(list1.equals(list0));
+    assertEquals(list0.hashCode(), list1.hashCode());
+
+    // Compare different sizes.
+    ColumnSortInfo info1 = createColumnSortInfo();
+    list0.push(info1);
+    assertFalse(list0.equals(list1));
+    assertFalse(list1.equals(list0));
+    assertFalse(list0.hashCode() == list1.hashCode());
+    list1.push(info1); // Make the lists equal again.
+
+    // Compare with different items that equals each other.
+    ColumnSortInfo info2a = createColumnSortInfo();
+    ColumnSortInfo info2b = new ColumnSortInfo(info2a.getColumn(),
+        info2a.isAscending());
+    list0.push(info2a);
+    list1.push(info2b);
+    assertTrue(list0.equals(list1));
+    assertTrue(list1.equals(list0));
+    assertEquals(list0.hashCode(), list1.hashCode());
+
+    // Compare same items, but out of order.
+    list0.push(info0);
+    assertFalse(list0.equals(list1));
+    assertFalse(list1.equals(list0));
+    assertFalse(list0.hashCode() == list1.hashCode());
+
+    // Compare to null.
+    assertFalse(list0.equals(null));
+    assertFalse(list1.equals(null));
+  }
+
+  public void testInsert() {
+    ColumnSortList list = new ColumnSortList();
+    assertEquals(0, list.size());
+
+    // Insert into an empty list.
+    ColumnSortInfo info0 = createColumnSortInfo();
+    list.insert(0, info0);
+    assertEquals(1, list.size());
+    assertEquals(info0, list.get(0));
+
+    // Insert null.
+    try {
+      list.insert(0, (ColumnSortInfo) null);
+      fail("Expected IllegalArgumentException.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+
+    // Insert the same item.
+    list.insert(0, info0);
+    assertEquals(1, list.size());
+    assertEquals(info0, list.get(0));
+
+    // Insert a second item at index 0.
+    ColumnSortInfo info1 = createColumnSortInfo();
+    list.insert(0, info1);
+    assertEquals(2, list.size());
+    assertEquals(info1, list.get(0));
+    assertEquals(info0, list.get(1));
+
+    // Insert a third item at the last index.
+    ColumnSortInfo info2 = createColumnSortInfo();
+    list.insert(list.size(), info2);
+    assertEquals(3, list.size());
+    assertEquals(info1, list.get(0));
+    assertEquals(info0, list.get(1));
+    assertEquals(info2, list.get(2));
+
+    // Insert item0 again. It should move to the new index.
+    list.insert(list.size(), info0);
+    assertEquals(3, list.size());
+    assertEquals(info1, list.get(0));
+    assertEquals(info2, list.get(1));
+    assertEquals(info0, list.get(2));
+  }
+
+  public void testPushColumn() {
+    ColumnSortList list = new ColumnSortList();
+    assertEquals(0, list.size());
+
+    // Push an item.
+    Column<String, String> col0 = new IdentityColumn<String>(new TextCell());
+    ColumnSortInfo item0 = list.push(col0);
+    assertEquals(1, list.size());
+    assertEquals(item0, list.get(0));
+    assertEquals(col0, list.get(0).getColumn());
+    assertTrue(list.get(0).isAscending());
+
+    // Push the same item. Should change sort order.
+    ColumnSortInfo item0desc = list.push(col0);
+    assertEquals(1, list.size());
+    assertEquals(item0desc, list.get(0));
+    assertEquals(col0, list.get(0).getColumn());
+    assertFalse(list.get(0).isAscending());
+
+    // Push a second item.
+    Column<String, String> col1 = new IdentityColumn<String>(new TextCell());
+    list.push(col1);
+    assertEquals(2, list.size());
+    assertEquals(col1, list.get(0).getColumn());
+    assertTrue(list.get(0).isAscending());
+    assertEquals(col0, list.get(1).getColumn());
+    assertFalse(list.get(1).isAscending());
+
+    // Push a third item.
+    Column<String, String> col2 = new IdentityColumn<String>(new TextCell());
+    list.push(col2);
+    assertEquals(3, list.size());
+    assertEquals(col2, list.get(0).getColumn());
+    assertTrue(list.get(0).isAscending());
+    assertEquals(col1, list.get(1).getColumn());
+    assertTrue(list.get(1).isAscending());
+    assertEquals(col0, list.get(2).getColumn());
+    assertFalse(list.get(2).isAscending());
+
+    // Push col0 again. Should move back to the front in ascending order.
+    list.push(col0);
+    assertEquals(3, list.size());
+    assertEquals(col0, list.get(0).getColumn());
+    assertTrue(list.get(0).isAscending());
+    assertEquals(col2, list.get(1).getColumn());
+    assertTrue(list.get(1).isAscending());
+    assertEquals(col1, list.get(2).getColumn());
+    assertTrue(list.get(2).isAscending());
+  }
+
+  public void testPushColumnSortInfo() {
+    ColumnSortList list = new ColumnSortList();
+    assertEquals(0, list.size());
+
+    // Push null.
+    try {
+      list.push((ColumnSortInfo) null);
+      fail("Expected IllegalArgumentException.");
+    } catch (IllegalArgumentException e) {
+      // Expected.
+    }
+
+    // Push an item.
+    ColumnSortInfo info0 = createColumnSortInfo();
+    list.push(info0);
+    assertEquals(1, list.size());
+    assertEquals(info0, list.get(0));
+
+    // Push the same item.
+    list.push(info0);
+    assertEquals(1, list.size());
+    assertEquals(info0, list.get(0));
+
+    // Push a second item.
+    ColumnSortInfo info1 = createColumnSortInfo();
+    list.push(info1);
+    assertEquals(2, list.size());
+    assertEquals(info1, list.get(0));
+    assertEquals(info0, list.get(1));
+
+    // Push a third item.
+    ColumnSortInfo info2 = createColumnSortInfo();
+    list.push(info2);
+    assertEquals(3, list.size());
+    assertEquals(info2, list.get(0));
+    assertEquals(info1, list.get(1));
+    assertEquals(info0, list.get(2));
+
+    // Push item0 again. Should move back to the front
+    list.push(info0);
+    assertEquals(3, list.size());
+    assertEquals(info0, list.get(0));
+    assertEquals(info2, list.get(1));
+    assertEquals(info1, list.get(2));
+
+    // Push a fourth item with the same column as item1. Should remove item1.
+    ColumnSortInfo info1b = new ColumnSortInfo(info1.getColumn(), false);
+    list.push(info1b);
+    assertEquals(3, list.size());
+    assertEquals(info1b, list.get(0));
+    assertEquals(info0, list.get(1));
+    assertEquals(info2, list.get(2));
+  }
+
+  /**
+   * Verify that the Column can be null.
+   */
+  public void testPushNullColumn() {
+    ColumnSortList list = new ColumnSortList();
+    assertEquals(0, list.size());
+
+    // Push an null column.
+    ColumnSortInfo info0 = list.push((Column<?, ?>) null);
+    assertEquals(1, list.size());
+    assertNull(info0.getColumn());
+    assertTrue(info0.isAscending());
+
+    // Push null again.
+    ColumnSortInfo info1 = list.push((Column<?, ?>) null);
+    assertEquals(1, list.size());
+    assertNull(info1.getColumn());
+    assertFalse(info1.isAscending());
+
+    // Push a non-null value.
+    ColumnSortInfo info2 = createColumnSortInfo();
+    list.push(info2);
+    assertEquals(2, list.size());
+    assertNull(list.get(1).getColumn());
+
+    // Push null again.
+    list.push((Column<?, ?>) null);
+    assertEquals(2, list.size());
+    assertNull(list.get(0).getColumn());
+    assertEquals(info2, list.get(1));
+  }
+
+  public void testRemove() {
+    ColumnSortList list = new ColumnSortList();
+
+    // Remove the only item.
+    ColumnSortInfo info = createColumnSortInfo();
+    list.push(info);
+    assertEquals(1, list.size());
+    assertTrue(list.remove(info));
+    assertEquals(0, list.size());
+
+    // Remove a middle item.
+    ColumnSortInfo info0 = createColumnSortInfo();
+    ColumnSortInfo info1 = createColumnSortInfo();
+    ColumnSortInfo info2 = createColumnSortInfo();
+    list.push(info0);
+    list.push(info1);
+    list.push(info2);
+    assertEquals(3, list.size());
+    assertTrue(list.remove(info1));
+    assertEquals(2, list.size());
+    assertEquals(info2, list.get(0));
+    assertEquals(info0, list.get(1));
+
+    // Remove an item that doesn't exist.
+    assertFalse(list.remove(createColumnSortInfo()));
+  }
+
+  /**
+   * Create a {@link ColumnSortInfo} with a unique column and cell.
+   * 
+   * @return a new {@link ColumnSortInfo}
+   */
+  private ColumnSortInfo createColumnSortInfo() {
+    return new ColumnSortInfo(new IdentityColumn<String>(new TextCell()), true);
+  }
+
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/user/cellview/client/ColumnTest.java b/user/test/com/google/gwt/user/cellview/client/ColumnTest.java
index 6190c90..5c9817f 100644
--- a/user/test/com/google/gwt/user/cellview/client/ColumnTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/ColumnTest.java
@@ -207,4 +207,16 @@
     column.render(context, "test", sb);
     assertEquals("test", sb.toSafeHtml().asString());
   }
+
+  public void testSetSortable() {
+    TextCell cell = new TextCell();
+    Column<String, String> column = new IdentityColumn<String>(cell);
+    assertFalse(column.isSortable());
+
+    column.setSortable(true);
+    assertTrue(column.isSortable());
+
+    column.setSortable(false);
+    assertFalse(column.isSortable());
+  }
 }