Enable the table selector in the DetailsView.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / client / ui / ListViewImpl.java
1 /*
2  * Copyright (C) 2010, 2011 Openismus GmbH
3  *
4  * This file is part of GWT-Glom.
5  *
6  * GWT-Glom is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or (at your
9  * option) any later version.
10  *
11  * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with GWT-Glom.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package org.glom.web.client.ui;
21
22 import java.util.ArrayList;
23
24 import org.glom.web.client.OnlineGlomServiceAsync;
25 import org.glom.web.client.place.DetailsPlace;
26 import org.glom.web.shared.GlomField;
27 import org.glom.web.shared.layout.LayoutGroup;
28 import org.glom.web.shared.layout.LayoutItem;
29 import org.glom.web.shared.layout.LayoutItemField;
30
31 import com.google.gwt.cell.client.AbstractCell;
32 import com.google.gwt.cell.client.ButtonCell;
33 import com.google.gwt.cell.client.CheckboxCell;
34 import com.google.gwt.cell.client.ValueUpdater;
35 import com.google.gwt.dom.client.Element;
36 import com.google.gwt.dom.client.InputElement;
37 import com.google.gwt.dom.client.NativeEvent;
38 import com.google.gwt.event.dom.client.KeyCodes;
39 import com.google.gwt.safehtml.shared.SafeHtml;
40 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
41 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
42 import com.google.gwt.user.cellview.client.CellTable;
43 import com.google.gwt.user.cellview.client.Column;
44 import com.google.gwt.user.cellview.client.ColumnSortEvent.AsyncHandler;
45 import com.google.gwt.user.cellview.client.ColumnSortList;
46 import com.google.gwt.user.cellview.client.ColumnSortList.ColumnSortInfo;
47 import com.google.gwt.user.cellview.client.SafeHtmlHeader;
48 import com.google.gwt.user.cellview.client.SimplePager;
49 import com.google.gwt.user.client.rpc.AsyncCallback;
50 import com.google.gwt.user.client.ui.Composite;
51 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
52 import com.google.gwt.user.client.ui.VerticalPanel;
53 import com.google.gwt.view.client.AsyncDataProvider;
54 import com.google.gwt.view.client.HasData;
55 import com.google.gwt.view.client.ProvidesKey;
56 import com.google.gwt.view.client.Range;
57
58 public class ListViewImpl extends Composite implements ListView {
59
60         private class GlomTextCell extends AbstractCell<GlomField> {
61
62                 // The SafeHtml class is used to escape strings to avoid XSS attacks. This is not strictly
63                 // necessary because the values aren't coming from a user but I'm using it anyway as a reminder.
64                 @Override
65                 public void render(Context context, GlomField value, SafeHtmlBuilder sb) {
66                         if (value == null) {
67                                 return;
68                         }
69
70                         // get the foreground and background colours if they're set
71                         SafeHtml fgcolour = null, bgcolour = null;
72                         String colour = value.getFGColour();
73                         if (colour == null) {
74                                 fgcolour = SafeHtmlUtils.fromSafeConstant("");
75                         } else {
76                                 fgcolour = SafeHtmlUtils.fromString("color:" + colour + ";");
77                         }
78                         colour = value.getBGColour();
79                         if (colour == null) {
80                                 bgcolour = SafeHtmlUtils.fromSafeConstant("");
81                         } else {
82                                 bgcolour = SafeHtmlUtils.fromString("background-color:" + colour + ";");
83                         }
84
85                         // set the text and colours
86                         sb.appendHtmlConstant("<div style=\"" + fgcolour.asString() + bgcolour.asString() + "\">");
87                         sb.append(SafeHtmlUtils.fromString(value.getText()));
88                         sb.appendHtmlConstant("</div>");
89                 }
90         }
91
92         final private VerticalPanel vPanel = new VerticalPanel();
93         final private SimplePager pager = new SimplePager(SimplePager.TextLocation.CENTER);
94         private Presenter presenter;
95
96         public ListViewImpl() {
97                 initWidget(vPanel);
98         }
99
100         @Override
101         public void setPresenter(Presenter presenter) {
102                 this.presenter = presenter;
103         }
104
105         @Override
106         public void setCellTable(final String documentID, final String tableName, LayoutGroup layoutGroup) {
107                 // This is not really in the MVP style but there are issues creating a re-usable CellTable with methods like
108                 // setColumnTitles(), setNumRows() etc. The biggest problem is that the column objects (new Column<GlomField[],
109                 // GlomField>(new GlomTextCell())) aren't destroyed when the column is removed from the CellTable and
110                 // IndexOutOfBounds exceptions are encountered with invalid array indexes trying access the data in this line:
111                 // return object[j]. There's probably a workaround that could be done to fix this but I'm leaving it until
112                 // there's a reason to fix it (performance, ease of testing, alternate implementation or otherwise).
113
114                 vPanel.clear();
115
116                 final int primaryKeyIndex = layoutGroup.getPrimaryKeyIndex();
117                 ProvidesKey<GlomField[]> keyProvider = new ProvidesKey<GlomField[]>() {
118                         @Override
119                         public Object getKey(GlomField[] item) {
120                                 return item[primaryKeyIndex].getText();
121                         }
122                 };
123
124                 final CellTable<GlomField[]> table = new CellTable<GlomField[]>(20, keyProvider);
125
126                 AsyncDataProvider<GlomField[]> dataProvider = new AsyncDataProvider<GlomField[]>() {
127                         @Override
128                         @SuppressWarnings("unchecked")
129                         protected void onRangeChanged(HasData<GlomField[]> display) {
130                                 // setup the callback object
131                                 final Range range = display.getVisibleRange();
132                                 final int start = range.getStart();
133                                 AsyncCallback<ArrayList<GlomField[]>> callback = new AsyncCallback<ArrayList<GlomField[]>>() {
134                                         public void onFailure(Throwable caught) {
135                                                 // FIXME: need to deal with failure
136                                                 System.out.println("AsyncCallback Failed: OnlineGlomService.getTableData()");
137                                         }
138
139                                         public void onSuccess(ArrayList<GlomField[]> result) {
140                                                 updateRowData(start, result);
141                                         }
142                                 };
143
144                                 // get data from the server
145                                 ColumnSortList colSortList = table.getColumnSortList();
146                                 if (colSortList.size() > 0) {
147                                         // ColumnSortEvent has been requested by the user
148                                         ColumnSortInfo info = colSortList.get(0);
149                                         OnlineGlomServiceAsync.Util.getInstance().getSortedListData(documentID, tableName, start,
150                                                         range.getLength(), table.getColumnIndex((Column<GlomField[], ?>) info.getColumn()),
151                                                         info.isAscending(), callback);
152                                 } else {
153                                         OnlineGlomServiceAsync.Util.getInstance().getListData(documentID, tableName, start,
154                                                         range.getLength(), callback);
155                                 }
156                         }
157                 };
158
159                 dataProvider.addDataDisplay(table);
160
161                 // create instances of GlomFieldColumn to retrieve the GlomField objects
162                 ArrayList<LayoutItem> layoutItems = layoutGroup.getItems();
163                 int numItems = layoutGroup.hasHiddenPrimaryKey() ? layoutItems.size() - 1 : layoutItems.size();
164                 for (int i = 0; i < numItems; i++) {
165                         LayoutItem layoutItem = layoutItems.get(i);
166
167                         // only create columns for LayoutItemField types
168                         if (!(layoutItem instanceof LayoutItemField)) {
169                                 continue;
170                         }
171                         LayoutItemField layoutItemField = (LayoutItemField) layoutItem;
172
173                         // create a new column
174                         Column<GlomField[], ?> column = null;
175                         final int j = new Integer(i);
176
177                         switch (layoutItemField.getType()) {
178                         case TYPE_BOOLEAN:
179                                 // The onBrowserEvent method is overridden to ensure the user can't toggle the checkbox. We'll probably
180                                 // be able to use a CheckboxCell directly when we add support for editing.
181                                 column = new Column<GlomField[], Boolean>(new CheckboxCell(false, false) {
182                                         @Override
183                                         public void onBrowserEvent(Context context, Element parent, Boolean value, NativeEvent event,
184                                                         ValueUpdater<Boolean> valueUpdater) {
185                                                 String type = event.getType();
186
187                                                 boolean enterPressed = "keydown".equals(type) && event.getKeyCode() == KeyCodes.KEY_ENTER;
188                                                 if ("change".equals(type) || enterPressed) {
189                                                         InputElement input = parent.getFirstChild().cast();
190                                                         input.setChecked(!input.isChecked());
191                                                 }
192                                         }
193                                 }) {
194                                         @Override
195                                         public Boolean getValue(GlomField[] object) {
196                                                 return object[j].getBoolean();
197                                         }
198                                 };
199                                 // Make checkboxes centred in the column.
200                                 column.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
201                                 break;
202                         default:
203                                 // use a text rendering cell for types we don't know about but log an error
204                                 // TODO log error here
205                         case TYPE_DATE:
206                         case TYPE_IMAGE:
207                         case TYPE_INVALID:
208                         case TYPE_NUMERIC:
209                         case TYPE_TIME:
210                         case TYPE_TEXT:
211                                 // All of these types are formatted as text in the servlet.
212                                 column = new Column<GlomField[], GlomField>(new GlomTextCell()) {
213                                         @Override
214                                         public GlomField getValue(GlomField[] object) {
215                                                 return object[j];
216                                         }
217                                 };
218
219                                 // Set the alignment of the text.
220                                 switch (layoutItemField.getFormatting().getHorizontalAlignment()) {
221                                 case HORIZONTAL_ALIGNMENT_LEFT:
222                                         column.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_LEFT);
223                                         break;
224                                 case HORIZONTAL_ALIGNMENT_RIGHT:
225                                         column.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT);
226                                         break;
227                                 case HORIZONTAL_ALIGNMENT_AUTO:
228                                 default:
229                                         // TODO: log warning, this shouldn't happen
230                                         column.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_DEFAULT);
231                                         break;
232                                 }
233                                 break;
234                         }
235
236                         // set column properties and add to cell table
237                         column.setSortable(true);
238                         table.addColumn(column, new SafeHtmlHeader(SafeHtmlUtils.fromString(layoutItemField.getTitle())));
239                 }
240
241                 Column<GlomField[], String> detailsColumn = new Column<GlomField[], String>(new ButtonCell() {
242                         /*
243                          * (non-Javadoc)
244                          * 
245                          * @see com.google.gwt.cell.client.ButtonCell#onEnterKeyDown(com.google.gwt.cell.client.Cell.Context,
246                          * com.google.gwt.dom.client.Element, java.lang.String, com.google.gwt.dom.client.NativeEvent,
247                          * com.google.gwt.cell.client.ValueUpdater)
248                          */
249                         @Override
250                         protected void onEnterKeyDown(Context context, Element parent, String value, NativeEvent event,
251                                         ValueUpdater<String> valueUpdater) {
252                                 super.onEnterKeyDown(context, parent, value, event, valueUpdater);
253
254                                 presenter.goTo(new DetailsPlace(documentID, tableName, (String) context.getKey()));
255                         }
256
257                 }) {
258                         @Override
259                         public String getValue(GlomField[] object) {
260                                 return "Details";
261                         }
262                 };
263
264                 table.addColumn(detailsColumn, "");
265
266                 // set row count which is needed for paging
267                 table.setRowCount(layoutGroup.getExpectedResultSize());
268
269                 // add an AsyncHandler to activate sorting for the AsyncDataProvider that was created above
270                 table.addColumnSortHandler(new AsyncHandler(table));
271
272                 pager.setDisplay(table);
273                 vPanel.add(table);
274                 vPanel.add(pager);
275         }
276
277         /*
278          * (non-Javadoc)
279          * 
280          * @see org.glom.web.client.ui.ListView#clear()
281          */
282         @Override
283         public void clear() {
284                 vPanel.clear();
285         }
286
287 }