1 /*
2  * Copyright 2011 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package com.google.gwt.sample.showcase.client.content.cell;
17 
18 import com.google.gwt.cell.client.Cell.Context;
19 import com.google.gwt.cell.client.CheckboxCell;
20 import com.google.gwt.cell.client.ClickableTextCell;
21 import com.google.gwt.cell.client.EditTextCell;
22 import com.google.gwt.cell.client.FieldUpdater;
23 import com.google.gwt.cell.client.NumberCell;
24 import com.google.gwt.cell.client.SelectionCell;
25 import com.google.gwt.cell.client.TextCell;
26 import com.google.gwt.core.client.GWT;
27 import com.google.gwt.core.client.RunAsyncCallback;
28 import com.google.gwt.dom.builder.shared.DivBuilder;
29 import com.google.gwt.dom.builder.shared.TableCellBuilder;
30 import com.google.gwt.dom.builder.shared.TableRowBuilder;
31 import com.google.gwt.dom.client.Style.Cursor;
32 import com.google.gwt.dom.client.Style.OutlineStyle;
33 import com.google.gwt.dom.client.Style.Unit;
34 import com.google.gwt.i18n.client.Constants;
35 import com.google.gwt.i18n.client.NumberFormat;
36 import com.google.gwt.resources.client.ClientBundle;
37 import com.google.gwt.resources.client.CssResource;
38 import com.google.gwt.safehtml.shared.SafeHtml;
39 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
40 import com.google.gwt.sample.showcase.client.ContentWidget;
41 import com.google.gwt.sample.showcase.client.ShowcaseAnnotations.ShowcaseData;
42 import com.google.gwt.sample.showcase.client.ShowcaseAnnotations.ShowcaseRaw;
43 import com.google.gwt.sample.showcase.client.ShowcaseAnnotations.ShowcaseSource;
44 import com.google.gwt.sample.showcase.client.content.cell.ContactDatabase.Category;
45 import com.google.gwt.sample.showcase.client.content.cell.ContactDatabase.ContactInfo;
46 import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;
47 import com.google.gwt.text.shared.SafeHtmlRenderer;
48 import com.google.gwt.uibinder.client.UiBinder;
49 import com.google.gwt.uibinder.client.UiField;
50 import com.google.gwt.user.cellview.client.AbstractCellTable.Style;
51 import com.google.gwt.user.cellview.client.AbstractCellTableBuilder;
52 import com.google.gwt.user.cellview.client.AbstractHeaderOrFooterBuilder;
53 import com.google.gwt.user.cellview.client.Column;
54 import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler;
55 import com.google.gwt.user.cellview.client.ColumnSortList;
56 import com.google.gwt.user.cellview.client.ColumnSortList.ColumnSortInfo;
57 import com.google.gwt.user.cellview.client.DataGrid;
58 import com.google.gwt.user.cellview.client.Header;
59 import com.google.gwt.user.cellview.client.SimplePager;
60 import com.google.gwt.user.cellview.client.SimplePager.TextLocation;
61 import com.google.gwt.user.cellview.client.TextHeader;
62 import com.google.gwt.user.client.rpc.AsyncCallback;
63 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
64 import com.google.gwt.user.client.ui.Label;
65 import com.google.gwt.user.client.ui.Widget;
66 import com.google.gwt.view.client.DefaultSelectionEventManager;
67 import com.google.gwt.view.client.MultiSelectionModel;
68 import com.google.gwt.view.client.SelectionModel;
69 
70 import java.util.ArrayList;
71 import java.util.Comparator;
72 import java.util.Date;
73 import java.util.HashSet;
74 import java.util.List;
75 import java.util.Set;
76 
77 /**
78  * Defines a custom table that displays a contact in each row. This is an
79  * example that shows how to completely customize the appearance of the headers,
80  * data rows, and footers in a CellTable.
81  */
82 @ShowcaseRaw({"ContactDatabase.java", "CwCustomDataGrid.ui.xml", "CwCustomDataGrid.gss"})
83 public class CwCustomDataGrid extends ContentWidget {
84 
85   /**
86    * The constants used in this Content Widget.
87    */
88   @ShowcaseSource
89   public static interface CwConstants extends Constants {
cwCustomDataGridColumnAddress()90     String cwCustomDataGridColumnAddress();
91 
cwCustomDataGridColumnAge()92     String cwCustomDataGridColumnAge();
93 
cwCustomDataGridColumnCategory()94     String cwCustomDataGridColumnCategory();
95 
cwCustomDataGridColumnFirstName()96     String cwCustomDataGridColumnFirstName();
97 
cwCustomDataGridColumnLastName()98     String cwCustomDataGridColumnLastName();
99 
cwCustomDataGridDescription()100     String cwCustomDataGridDescription();
101 
cwCustomDataGridEmpty()102     String cwCustomDataGridEmpty();
103 
cwCustomDataGridName()104     String cwCustomDataGridName();
105   }
106 
107   /**
108    * The UiBinder interface used by this example.
109    */
110   @ShowcaseSource
111   interface Binder extends UiBinder<Widget, CwCustomDataGrid> {
112   }
113 
114   /**
115    * The resources used by this example.
116    */
117   @ShowcaseSource
118   interface Resources extends ClientBundle {
119 
120     /**
121      * Get the styles used but this example.
122      */
123     @Source("CwCustomDataGrid.gss")
styles()124     Styles styles();
125   }
126 
127   /**
128    * The CSS Resources used by this example.
129    */
130   @ShowcaseSource
131   interface Styles extends CssResource {
132     /**
133      * Indents cells in child rows.
134      */
childCell()135     String childCell();
136 
137     /**
138      * Applies to group headers.
139      */
groupHeaderCell()140     String groupHeaderCell();
141   }
142 
143   /**
144    * Renders custom table headers. The top header row includes the groups "Name"
145    * and "Information", each of which spans multiple columns. The second row of
146    * the headers includes the contacts' first and last names grouped under the
147    * "Name" category. The second row also includes the age, category, and
148    * address of the contacts grouped under the "Information" category.
149    */
150   @ShowcaseSource
151   private class CustomHeaderBuilder extends AbstractHeaderOrFooterBuilder<ContactInfo> {
152 
153     private Header<String> firstNameHeader = new TextHeader(constants
154         .cwCustomDataGridColumnFirstName());
155     private Header<String> lastNameHeader = new TextHeader(constants
156         .cwCustomDataGridColumnLastName());
157     private Header<String> ageHeader = new TextHeader(constants.cwCustomDataGridColumnAge());
158     private Header<String> categoryHeader = new TextHeader(constants
159         .cwCustomDataGridColumnCategory());
160     private Header<String> addressHeader =
161         new TextHeader(constants.cwCustomDataGridColumnAddress());
162 
CustomHeaderBuilder()163     public CustomHeaderBuilder() {
164       super(dataGrid, false);
165       setSortIconStartOfLine(false);
166     }
167 
168     @Override
buildHeaderOrFooterImpl()169     protected boolean buildHeaderOrFooterImpl() {
170       Style style = dataGrid.getResources().style();
171       String groupHeaderCell = resources.styles().groupHeaderCell();
172 
173       // Add a 2x2 header above the checkbox and show friends columns.
174       TableRowBuilder tr = startRow();
175       tr.startTH().colSpan(2).rowSpan(2)
176           .className(style.header() + " " + style.firstColumnHeader());
177       tr.endTH();
178 
179       /*
180        * Name group header. Associated with the last name column, so clicking on
181        * the group header sorts by last name.
182        */
183       TableCellBuilder th = tr.startTH().colSpan(2).className(groupHeaderCell);
184       enableColumnHandlers(th, lastNameColumn);
185       th.style().trustedProperty("border-right", "10px solid white").cursor(Cursor.POINTER)
186           .endStyle();
187       th.text("Name").endTH();
188 
189       // Information group header.
190       th = tr.startTH().colSpan(3).className(groupHeaderCell);
191       th.text("Information").endTH();
192 
193       // Get information about the sorted column.
194       ColumnSortList sortList = dataGrid.getColumnSortList();
195       ColumnSortInfo sortedInfo = (sortList.size() == 0) ? null : sortList.get(0);
196       Column<?, ?> sortedColumn = (sortedInfo == null) ? null : sortedInfo.getColumn();
197       boolean isSortAscending = (sortedInfo == null) ? false : sortedInfo.isAscending();
198 
199       // Add column headers.
200       tr = startRow();
201       buildHeader(tr, firstNameHeader, firstNameColumn, sortedColumn, isSortAscending, false, false);
202       buildHeader(tr, lastNameHeader, lastNameColumn, sortedColumn, isSortAscending, false, false);
203       buildHeader(tr, ageHeader, ageColumn, sortedColumn, isSortAscending, false, false);
204       buildHeader(tr, categoryHeader, categoryColumn, sortedColumn, isSortAscending, false, false);
205       buildHeader(tr, addressHeader, addressColumn, sortedColumn, isSortAscending, false, true);
206       tr.endTR();
207 
208       return true;
209     }
210 
211     /**
212      * Renders the header of one column, with the given options.
213      *
214      * @param out the table row to build into
215      * @param header the {@link Header} to render
216      * @param column the column to associate with the header
217      * @param sortedColumn the column that is currently sorted
218      * @param isSortAscending true if the sorted column is in ascending order
219      * @param isFirst true if this the first column
220      * @param isLast true if this the last column
221      */
buildHeader(TableRowBuilder out, Header<?> header, Column<ContactInfo, ?> column, Column<?, ?> sortedColumn, boolean isSortAscending, boolean isFirst, boolean isLast)222     private void buildHeader(TableRowBuilder out, Header<?> header, Column<ContactInfo, ?> column,
223         Column<?, ?> sortedColumn, boolean isSortAscending, boolean isFirst, boolean isLast) {
224       // Choose the classes to include with the element.
225       Style style = dataGrid.getResources().style();
226       boolean isSorted = (sortedColumn == column);
227       StringBuilder classesBuilder = new StringBuilder(style.header());
228       if (isFirst) {
229         classesBuilder.append(" " + style.firstColumnHeader());
230       }
231       if (isLast) {
232         classesBuilder.append(" " + style.lastColumnHeader());
233       }
234       if (column.isSortable()) {
235         classesBuilder.append(" " + style.sortableHeader());
236       }
237       if (isSorted) {
238         classesBuilder.append(" "
239             + (isSortAscending ? style.sortedHeaderAscending() : style.sortedHeaderDescending()));
240       }
241 
242       // Create the table cell.
243       TableCellBuilder th = out.startTH().className(classesBuilder.toString());
244 
245       // Associate the cell with the column to enable sorting of the column.
246       enableColumnHandlers(th, column);
247 
248       // Render the header.
249       Context context = new Context(0, 2, header.getKey());
250       renderSortableHeader(th, context, header, isSorted, isSortAscending);
251 
252       // End the table cell.
253       th.endTH();
254     }
255   }
256 
257   /**
258    * Renders custom table footers that appear beneath the columns in the table.
259    * This footer consists of a single cell containing the average age of all
260    * contacts on the current page. This is an example of a dynamic footer that
261    * changes with the row data in the table.
262    */
263   @ShowcaseSource
264   private class CustomFooterBuilder extends AbstractHeaderOrFooterBuilder<ContactInfo> {
265 
CustomFooterBuilder()266     public CustomFooterBuilder() {
267       super(dataGrid, true);
268     }
269 
270     @Override
buildHeaderOrFooterImpl()271     protected boolean buildHeaderOrFooterImpl() {
272       String footerStyle = dataGrid.getResources().style().footer();
273 
274       // Calculate the age of all visible contacts.
275       String ageStr = "";
276       List<ContactInfo> items = dataGrid.getVisibleItems();
277       if (items.size() > 0) {
278         int totalAge = 0;
279         for (ContactInfo item : items) {
280           totalAge += item.getAge();
281         }
282         ageStr = "Avg: " + totalAge / items.size();
283       }
284 
285       // Cells before age column.
286       TableRowBuilder tr = startRow();
287       tr.startTH().colSpan(4).className(footerStyle).endTH();
288 
289       // Show the average age of all contacts.
290       TableCellBuilder th =
291           tr.startTH().className(footerStyle).align(
292               HasHorizontalAlignment.ALIGN_CENTER.getTextAlignString());
293       th.text(ageStr);
294       th.endTH();
295 
296       // Cells after age column.
297       tr.startTH().colSpan(2).className(footerStyle).endTH();
298       tr.endTR();
299 
300       return true;
301     }
302   }
303 
304   /**
305    * Renders the data rows that display each contact in the table.
306    */
307   @ShowcaseSource
308   private class CustomTableBuilder extends AbstractCellTableBuilder<ContactInfo> {
309 
310     private final int todayMonth;
311 
312     private final String childCell = " " + resources.styles().childCell();
313     private final String rowStyle;
314     private final String selectedRowStyle;
315     private final String cellStyle;
316     private final String selectedCellStyle;
317 
318     @SuppressWarnings("deprecation")
CustomTableBuilder()319     public CustomTableBuilder() {
320       super(dataGrid);
321 
322       // Cache styles for faster access.
323       Style style = dataGrid.getResources().style();
324       rowStyle = style.evenRow();
325       selectedRowStyle = " " + style.selectedRow();
326       cellStyle = style.cell() + " " + style.evenRowCell();
327       selectedCellStyle = " " + style.selectedRowCell();
328 
329       // Record today's date.
330       Date today = new Date();
331       todayMonth = today.getMonth();
332     }
333 
334     @SuppressWarnings("deprecation")
335     @Override
buildRowImpl(ContactInfo rowValue, int absRowIndex)336     public void buildRowImpl(ContactInfo rowValue, int absRowIndex) {
337       buildContactRow(rowValue, absRowIndex, false);
338 
339       // Display information about the user in another row that spans the entire
340       // table.
341       Date dob = rowValue.getBirthday();
342       if (dob.getMonth() == todayMonth) {
343         TableRowBuilder row = startRow();
344         TableCellBuilder td = row.startTD().colSpan(7).className(cellStyle);
345         td.style().trustedBackgroundColor("#ccf").endStyle();
346         td.text(rowValue.getFirstName() + "'s birthday is this month!").endTD();
347         row.endTR();
348       }
349 
350       // Display list of friends.
351       if (showingFriends.contains(rowValue.getId())) {
352         Set<ContactInfo> friends = ContactDatabase.get().queryFriends(rowValue);
353         for (ContactInfo friend : friends) {
354           buildContactRow(friend, absRowIndex, true);
355         }
356       }
357     }
358 
359     /**
360      * Build a row.
361      *
362      * @param rowValue the contact info
363      * @param absRowIndex the absolute row index
364      * @param isFriend true if this is a subrow, false if a top level row
365      */
366     @SuppressWarnings("deprecation")
buildContactRow(ContactInfo rowValue, int absRowIndex, boolean isFriend)367     private void buildContactRow(ContactInfo rowValue, int absRowIndex, boolean isFriend) {
368       // Calculate the row styles.
369       SelectionModel<? super ContactInfo> selectionModel = dataGrid.getSelectionModel();
370       boolean isSelected =
371           (selectionModel == null || rowValue == null) ? false : selectionModel
372               .isSelected(rowValue);
373       boolean isEven = absRowIndex % 2 == 0;
374       StringBuilder trClasses = new StringBuilder(rowStyle);
375       if (isSelected) {
376         trClasses.append(selectedRowStyle);
377       }
378 
379       // Calculate the cell styles.
380       String cellStyles = cellStyle;
381       if (isSelected) {
382         cellStyles += selectedCellStyle;
383       }
384       if (isFriend) {
385         cellStyles += childCell;
386       }
387 
388       TableRowBuilder row = startRow();
389       row.className(trClasses.toString());
390 
391       /*
392        * Checkbox column.
393        *
394        * This table will uses a checkbox column for selection. Alternatively,
395        * you can call dataGrid.setSelectionEnabled(true) to enable mouse
396        * selection.
397        */
398       TableCellBuilder td = row.startTD();
399       td.className(cellStyles);
400       td.style().outlineStyle(OutlineStyle.NONE).endStyle();
401       if (!isFriend) {
402         renderCell(td, createContext(0), checkboxColumn, rowValue);
403       }
404       td.endTD();
405 
406       /*
407        * View friends column.
408        *
409        * Displays a link to "show friends". When clicked, the list of friends is
410        * displayed below the contact.
411        */
412       td = row.startTD();
413       td.className(cellStyles);
414       if (!isFriend) {
415         td.style().outlineStyle(OutlineStyle.NONE).endStyle();
416         renderCell(td, createContext(1), viewFriendsColumn, rowValue);
417       }
418       td.endTD();
419 
420       // First name column.
421       td = row.startTD();
422       td.className(cellStyles);
423       td.style().outlineStyle(OutlineStyle.NONE).endStyle();
424       if (isFriend) {
425         td.text(rowValue.getFirstName());
426       } else {
427         renderCell(td, createContext(2), firstNameColumn, rowValue);
428       }
429       td.endTD();
430 
431       // Last name column.
432       td = row.startTD();
433       td.className(cellStyles);
434       td.style().outlineStyle(OutlineStyle.NONE).endStyle();
435       if (isFriend) {
436         td.text(rowValue.getLastName());
437       } else {
438         renderCell(td, createContext(3), lastNameColumn, rowValue);
439       }
440       td.endTD();
441 
442       // Age column.
443       td = row.startTD();
444       td.className(cellStyles);
445       td.style().outlineStyle(OutlineStyle.NONE).endStyle();
446       td.text(NumberFormat.getDecimalFormat().format(rowValue.getAge())).endTD();
447 
448       // Category column.
449       td = row.startTD();
450       td.className(cellStyles);
451       td.style().outlineStyle(OutlineStyle.NONE).endStyle();
452       if (isFriend) {
453         td.text(rowValue.getCategory().getDisplayName());
454       } else {
455         renderCell(td, createContext(5), categoryColumn, rowValue);
456       }
457       td.endTD();
458 
459       // Address column.
460       td = row.startTD();
461       td.className(cellStyles);
462       DivBuilder div = td.startDiv();
463       div.style().outlineStyle(OutlineStyle.NONE).endStyle();
464       div.text(rowValue.getAddress()).endDiv();
465       td.endTD();
466 
467       row.endTR();
468     }
469   }
470 
471   /**
472    * The main DataGrid.
473    */
474   @ShowcaseData
475   @UiField(provided = true)
476   DataGrid<ContactInfo> dataGrid;
477 
478   /**
479    * The pager used to change the range of data.
480    */
481   @ShowcaseData
482   @UiField(provided = true)
483   SimplePager pager;
484 
485   /**
486    * An instance of the constants.
487    */
488   @ShowcaseData
489   private final CwConstants constants;
490 
491   /**
492    * The resources used by this example.
493    */
494   @ShowcaseData
495   private Resources resources;
496 
497   /**
498    * Contains the contact id for each row in the table where the friends list is
499    * currently expanded.
500    */
501   @ShowcaseData
502   private final Set<Integer> showingFriends = new HashSet<Integer>();
503 
504   /**
505    * Column to control selection.
506    */
507   @ShowcaseData
508   private Column<ContactInfo, Boolean> checkboxColumn;
509 
510   /**
511    * Column to expand friends list.
512    */
513   @ShowcaseData
514   private Column<ContactInfo, String> viewFriendsColumn;
515 
516   /**
517    * Column displays first name.
518    */
519   @ShowcaseData
520   private Column<ContactInfo, String> firstNameColumn;
521 
522   /**
523    * Column displays last name.
524    */
525   @ShowcaseData
526   private Column<ContactInfo, String> lastNameColumn;
527 
528   /**
529    * Column displays age.
530    */
531   @ShowcaseData
532   private Column<ContactInfo, Number> ageColumn;
533 
534   /**
535    * Column displays category.
536    */
537   @ShowcaseData
538   private Column<ContactInfo, String> categoryColumn;
539 
540   /**
541    * Column displays address.
542    */
543   @ShowcaseData
544   private Column<ContactInfo, String> addressColumn;
545 
546   /**
547    * Constructor.
548    *
549    * @param constants the constants
550    */
CwCustomDataGrid(CwConstants constants)551   public CwCustomDataGrid(CwConstants constants) {
552     super(constants.cwCustomDataGridName(), constants.cwCustomDataGridDescription(), false,
553         "ContactDatabase.java", "CwCustomDataGrid.ui.xml", "CwCustomDataGrid.css");
554     this.constants = constants;
555   }
556 
557   @Override
hasMargins()558   public boolean hasMargins() {
559     return false;
560   }
561 
562   @Override
hasScrollableContent()563   public boolean hasScrollableContent() {
564     return false;
565   }
566 
567   /**
568    * Initialize this example.
569    */
570   @ShowcaseSource
571   @Override
onInitialize()572   public Widget onInitialize() {
573     resources = GWT.create(Resources.class);
574     resources.styles().ensureInjected();
575 
576     // Create a DataGrid.
577 
578     /*
579      * Set a key provider that provides a unique key for each contact. If key is
580      * used to identify contacts when fields (such as the name and address)
581      * change.
582      */
583     dataGrid = new DataGrid<ContactInfo>(ContactDatabase.ContactInfo.KEY_PROVIDER);
584     dataGrid.setWidth("100%");
585 
586     /*
587      * Do not refresh the headers every time the data is updated. The footer
588      * depends on the current data, so we do not disable auto refresh on the
589      * footer.
590      */
591     dataGrid.setAutoHeaderRefreshDisabled(true);
592 
593     // Set the message to display when the table is empty.
594     dataGrid.setEmptyTableWidget(new Label(constants.cwCustomDataGridEmpty()));
595 
596     // Attach a column sort handler to the ListDataProvider to sort the list.
597     ListHandler<ContactInfo> sortHandler =
598         new ListHandler<ContactInfo>(ContactDatabase.get().getDataProvider().getList());
599     dataGrid.addColumnSortHandler(sortHandler);
600 
601     // Create a Pager to control the table.
602     SimplePager.Resources pagerResources = GWT.create(SimplePager.Resources.class);
603     pager = new SimplePager(TextLocation.CENTER, pagerResources, false, 0, true);
604     pager.setDisplay(dataGrid);
605 
606     // Add a selection model so we can select cells.
607     final SelectionModel<ContactInfo> selectionModel =
608         new MultiSelectionModel<ContactInfo>(ContactDatabase.ContactInfo.KEY_PROVIDER);
609     dataGrid.setSelectionModel(selectionModel, DefaultSelectionEventManager
610         .<ContactInfo> createCheckboxManager());
611 
612     // Initialize the columns.
613     initializeColumns(sortHandler);
614 
615     // Specify a custom table.
616     dataGrid.setTableBuilder(new CustomTableBuilder());
617     dataGrid.setHeaderBuilder(new CustomHeaderBuilder());
618     dataGrid.setFooterBuilder(new CustomFooterBuilder());
619 
620     // Add the CellList to the adapter in the database.
621     ContactDatabase.get().addDataDisplay(dataGrid);
622 
623     // Create the UiBinder.
624     Binder uiBinder = GWT.create(Binder.class);
625     return uiBinder.createAndBindUi(this);
626   }
627 
628   @Override
asyncOnInitialize(final AsyncCallback<Widget> callback)629   protected void asyncOnInitialize(final AsyncCallback<Widget> callback) {
630     GWT.runAsync(CwCustomDataGrid.class, new RunAsyncCallback() {
631 
632       @Override
633       public void onFailure(Throwable caught) {
634         callback.onFailure(caught);
635       }
636 
637       @Override
638       public void onSuccess() {
639         callback.onSuccess(onInitialize());
640       }
641     });
642   }
643 
644   /**
645    * Defines the columns in the custom table. Maps the data in the ContactInfo
646    * for each row into the appropriate column in the table, and defines handlers
647    * for each column.
648    */
649   @ShowcaseSource
initializeColumns(ListHandler<ContactInfo> sortHandler)650   private void initializeColumns(ListHandler<ContactInfo> sortHandler) {
651     /*
652      * Checkbox column.
653      *
654      * This table will uses a checkbox column for selection. Alternatively, you
655      * can call dataGrid.setSelectionEnabled(true) to enable mouse selection.
656      */
657     checkboxColumn = new Column<ContactInfo, Boolean>(new CheckboxCell(true, false)) {
658       @Override
659       public Boolean getValue(ContactInfo object) {
660         // Get the value from the selection model.
661         return dataGrid.getSelectionModel().isSelected(object);
662       }
663     };
664     dataGrid.addColumn(checkboxColumn);
665     dataGrid.setColumnWidth(0, 40, Unit.PX);
666 
667     // View friends.
668     SafeHtmlRenderer<String> anchorRenderer = new AbstractSafeHtmlRenderer<String>() {
669       @Override
670       public SafeHtml render(String object) {
671         SafeHtmlBuilder sb = new SafeHtmlBuilder();
672         sb.appendHtmlConstant("(<a href=\"javascript:;\">").appendEscaped(object)
673             .appendHtmlConstant("</a>)");
674         return sb.toSafeHtml();
675       }
676     };
677     viewFriendsColumn = new Column<ContactInfo, String>(new ClickableTextCell(anchorRenderer)) {
678       @Override
679       public String getValue(ContactInfo object) {
680         if (showingFriends.contains(object.getId())) {
681           return "hide friends";
682         } else {
683           return "show friends";
684         }
685       }
686     };
687     viewFriendsColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
688       @Override
689       public void update(int index, ContactInfo object, String value) {
690         if (showingFriends.contains(object.getId())) {
691           showingFriends.remove(object.getId());
692         } else {
693           showingFriends.add(object.getId());
694         }
695 
696         // Redraw the modified row.
697         dataGrid.redrawRow(index);
698       }
699     });
700     dataGrid.addColumn(viewFriendsColumn);
701     dataGrid.setColumnWidth(1, 10, Unit.EM);
702 
703     // First name.
704     firstNameColumn = new Column<ContactInfo, String>(new EditTextCell()) {
705       @Override
706       public String getValue(ContactInfo object) {
707         return object.getFirstName();
708       }
709     };
710     firstNameColumn.setSortable(true);
711     sortHandler.setComparator(firstNameColumn, new Comparator<ContactInfo>() {
712       @Override
713       public int compare(ContactInfo o1, ContactInfo o2) {
714         return o1.getFirstName().compareTo(o2.getFirstName());
715       }
716     });
717     firstNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
718       @Override
719       public void update(int index, ContactInfo object, String value) {
720         // Called when the user changes the value.
721         object.setFirstName(value);
722         ContactDatabase.get().refreshDisplays();
723       }
724     });
725     dataGrid.addColumn(firstNameColumn);
726     dataGrid.setColumnWidth(2, 20, Unit.PCT);
727 
728     // Last name.
729     lastNameColumn = new Column<ContactInfo, String>(new EditTextCell()) {
730       @Override
731       public String getValue(ContactInfo object) {
732         return object.getLastName();
733       }
734     };
735     lastNameColumn.setSortable(true);
736     sortHandler.setComparator(lastNameColumn, new Comparator<ContactInfo>() {
737       @Override
738       public int compare(ContactInfo o1, ContactInfo o2) {
739         return o1.getLastName().compareTo(o2.getLastName());
740       }
741     });
742     lastNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
743       @Override
744       public void update(int index, ContactInfo object, String value) {
745         // Called when the user changes the value.
746         object.setLastName(value);
747         ContactDatabase.get().refreshDisplays();
748       }
749     });
750     dataGrid.addColumn(lastNameColumn);
751     dataGrid.setColumnWidth(3, 20, Unit.PCT);
752 
753     // Age.
754     ageColumn = new Column<ContactInfo, Number>(new NumberCell()) {
755       @Override
756       public Number getValue(ContactInfo object) {
757         return object.getAge();
758       }
759     };
760     ageColumn.setSortable(true);
761     sortHandler.setComparator(ageColumn, new Comparator<ContactInfo>() {
762       @Override
763       public int compare(ContactInfo o1, ContactInfo o2) {
764         return o1.getAge() - o2.getAge();
765       }
766     });
767     dataGrid.addColumn(ageColumn);
768     dataGrid.setColumnWidth(4, 7, Unit.EM);
769 
770     // Category.
771     final Category[] categories = ContactDatabase.get().queryCategories();
772     List<String> categoryNames = new ArrayList<String>();
773     for (Category category : categories) {
774       categoryNames.add(category.getDisplayName());
775     }
776     SelectionCell categoryCell = new SelectionCell(categoryNames);
777     categoryColumn = new Column<ContactInfo, String>(categoryCell) {
778       @Override
779       public String getValue(ContactInfo object) {
780         return object.getCategory().getDisplayName();
781       }
782     };
783     categoryColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
784       @Override
785       public void update(int index, ContactInfo object, String value) {
786         for (Category category : categories) {
787           if (category.getDisplayName().equals(value)) {
788             object.setCategory(category);
789           }
790         }
791         ContactDatabase.get().refreshDisplays();
792       }
793     });
794     dataGrid.addColumn(categoryColumn);
795     dataGrid.setColumnWidth(5, 130, Unit.PX);
796 
797     // Address.
798     addressColumn = new Column<ContactInfo, String>(new TextCell()) {
799       @Override
800       public String getValue(ContactInfo object) {
801         return object.getAddress();
802       }
803     };
804     addressColumn.setSortable(true);
805     sortHandler.setComparator(addressColumn, new Comparator<ContactInfo>() {
806       @Override
807       public int compare(ContactInfo o1, ContactInfo o2) {
808         return o1.getAddress().compareTo(o2.getAddress());
809       }
810     });
811     dataGrid.addColumn(addressColumn);
812     dataGrid.setColumnWidth(6, 60, Unit.PCT);
813   }
814 }
815