1package autotest.common.table;
2
3
4import autotest.common.Utils;
5import autotest.common.ui.RightClickTable;
6
7import com.google.gwt.event.dom.client.ClickEvent;
8import com.google.gwt.event.dom.client.ClickHandler;
9import com.google.gwt.event.dom.client.ContextMenuEvent;
10import com.google.gwt.event.dom.client.ContextMenuHandler;
11import com.google.gwt.event.dom.client.DomEvent;
12import com.google.gwt.json.client.JSONObject;
13import com.google.gwt.json.client.JSONValue;
14import com.google.gwt.user.client.ui.Composite;
15import com.google.gwt.user.client.ui.HTMLTable;
16import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
17import com.google.gwt.user.client.ui.Widget;
18
19import java.util.ArrayList;
20import java.util.Collections;
21import java.util.List;
22
23/**
24 * A table to display data from JSONObjects.  Each row displays data from one
25 * JSONObject.  A header row with column titles is automatically generated, and
26 * support is included for adding other arbitrary header rows.
27 * <br><br>
28 * Styles:
29 * <ul>
30 * <li>.data-table - the entire table
31 * <li>.data-row-header - the column title row
32 * <li>.data-row-one/.data-row-two - data row styles.  These two are alternated.
33 * </ul>
34 */
35public class DataTable extends Composite implements ClickHandler, ContextMenuHandler {
36    public static final String HEADER_STYLE = "data-row-header";
37    public static final String CLICKABLE_STYLE = "data-row-clickable";
38    public static final String HIGHLIGHTED_STYLE = "data-row-highlighted";
39    public static final String WIDGET_COLUMN = "_WIDGET_COLUMN_";
40    // use CLICKABLE_WIDGET_COLUMN for widget that expect to receive clicks.  The table will ignore
41    // click events coming from these columns.
42    public static final String CLICKABLE_WIDGET_COLUMN = "_CLICKABLE_WIDGET_COLUMN_";
43    // for indexing into column subarrays (i.e. columns[1][COL_NAME])
44    public static final int COL_NAME = 0, COL_TITLE = 1;
45
46    public static interface DataTableListener {
47        public void onRowClicked(int rowIndex, JSONObject row, boolean isRightClick);
48    }
49
50    protected RightClickTable table;
51
52    protected String[][] columns;
53    protected int headerRow = 0;
54    protected boolean clickable = false;
55
56    protected TableWidgetFactory widgetFactory = null;
57    private List<DataTableListener> listeners = new ArrayList<DataTableListener>();
58
59    // keep a list of JSONObjects corresponding to rows in the table
60    protected List<JSONObject> jsonObjects = new ArrayList<JSONObject>();
61
62
63    public static interface TableWidgetFactory {
64        public Widget createWidget(int row, int cell, JSONObject rowObject);
65    }
66
67    /**
68     * @param columns An array specifying the name of each column and the field
69     * to which it corresponds.  The array should have the form
70     * {{'field_name1', 'Column Title 1'},
71     *  {'field_name2', 'Column Title 2'}, ...}.
72     */
73    public DataTable(String[][] columns) {
74        int rows = columns.length;
75        this.columns = new String[rows][2];
76        for (int i = 0; i < rows; i++) {
77            System.arraycopy(columns[i], 0, this.columns[i], 0, 2);
78        }
79
80        table = new RightClickTable();
81        initWidget(table);
82
83        table.setCellSpacing(0);
84        table.setCellPadding(0);
85        table.setStylePrimaryName("data-table");
86        table.addStyleDependentName("outlined");
87
88        for (int i = 0; i < columns.length; i++) {
89            table.setText(0, i, columns[i][1]);
90        }
91
92        table.getRowFormatter().setStylePrimaryName(0, HEADER_STYLE);
93        table.addClickHandler(this);
94    }
95
96    /**
97     * Causes the last column of the data table to fill the remainder of the width left in the
98     * parent widget.
99     */
100    public void fillParent() {
101        table.getColumnFormatter().setWidth(table.getCellCount(0) - 1, "100%");
102    }
103
104    public void setWidgetFactory(TableWidgetFactory widgetFactory) {
105        this.widgetFactory = widgetFactory;
106    }
107
108    protected void setRowStyle(int row) {
109        table.getRowFormatter().setStyleName(row, "data-row");
110        if ((row & 1) == 0) {
111            table.getRowFormatter().addStyleName(row, "data-row-alternate");
112        }
113        if (clickable) {
114            table.getRowFormatter().addStyleName(row, CLICKABLE_STYLE);
115        }
116    }
117
118    public void setClickable(boolean clickable) {
119        this.clickable = clickable;
120        for(int i = headerRow + 1; i < table.getRowCount(); i++)
121            setRowStyle(i);
122    }
123
124    /**
125     * Clear all data rows from the table.  Leaves the header rows intact.
126     */
127    public void clear() {
128        while (table.getRowCount() > 1) {
129            table.removeRow(1);
130        }
131        jsonObjects.clear();
132    }
133
134    /**
135     * This gets called for every JSONObject that gets added to the table using
136     * addRow().  This allows subclasses to customize objects before they are
137     * added to the table, for example to reformat fields or generate new
138     * fields from the existing data.
139     * @param row The row object about to be added to the table.
140     */
141    protected void preprocessRow(JSONObject row) {}
142
143    protected String[] getRowText(JSONObject row) {
144        String[] rowText = new String[columns.length];
145        for (int i = 0; i < columns.length; i++) {
146            if (isWidgetColumn(i))
147                continue;
148
149            String columnKey = columns[i][0];
150            JSONValue columnValue = row.get(columnKey);
151            if (columnValue == null || columnValue.isNull() != null) {
152                rowText[i] = "";
153            } else {
154                rowText[i] = Utils.jsonToString(columnValue);
155            }
156        }
157        return rowText;
158    }
159
160    /**
161     * Add a row from an array of Strings, one String for each column.
162     * @param rowData Data for each column, in left-to-right column order.
163     */
164    protected void addRowFromData(String[] rowData) {
165        int row = table.getRowCount();
166        for(int i = 0; i < columns.length; i++) {
167            if(isWidgetColumn(i)) {
168                table.setWidget(row, i, getWidgetForCell(row, i));
169            } else {
170                table.setText(row, i, rowData[i]);
171            }
172        }
173        setRowStyle(row);
174    }
175
176    protected boolean isWidgetColumn(int column) {
177        return columns[column][COL_NAME].equals(WIDGET_COLUMN) || isClickableWidgetColumn(column);
178    }
179
180    protected boolean isClickableWidgetColumn(int column) {
181        return columns[column][COL_NAME].equals(CLICKABLE_WIDGET_COLUMN);
182    }
183
184    /**
185     * Add a row from a JSONObject.  Columns will be populated by pulling fields
186     * from the objects, as dictated by the columns information passed into the
187     * DataTable constructor.
188     */
189    public void addRow(JSONObject row) {
190        preprocessRow(row);
191        jsonObjects.add(row);
192        addRowFromData(getRowText(row));
193    }
194
195    /**
196     * Add all objects in a JSONArray.
197     * @param rows An array of JSONObjects
198     * @throws IllegalArgumentException if any other type of JSONValue is in the
199     * array.
200     */
201    public void addRows(List<JSONObject> rows) {
202        for (JSONObject row : rows) {
203            addRow(row);
204        }
205    }
206
207    /**
208     * Remove a data row from the table.
209     * @param rowIndex The index of the row, where the first data row is indexed 0.
210     * Header rows are ignored.
211     */
212    public void removeRow(int rowIndex) {
213        jsonObjects.remove(rowIndex);
214        int realRow = rowIndex + 1; // header row
215        table.removeRow(realRow);
216        for(int i = realRow; i < table.getRowCount(); i++)
217            setRowStyle(i);
218    }
219
220    /**
221     * Returns the number of data rows in the table.  The actual number of
222     * visible table rows is more than this, due to the header row.
223     */
224    public int getRowCount() {
225        return table.getRowCount() - 1;
226    }
227
228    /**
229     * Get the JSONObject corresponding to the indexed row.
230     */
231    public JSONObject getRow(int rowIndex) {
232        return jsonObjects.get(rowIndex);
233    }
234
235    public List<JSONObject> getAllRows() {
236        return Collections.unmodifiableList(jsonObjects);
237    }
238
239    public void highlightRow(int row) {
240        row++; // account for header row
241        table.getRowFormatter().addStyleName(row, HIGHLIGHTED_STYLE);
242    }
243
244    public void unhighlightRow(int row) {
245        row++; // account for header row
246        table.getRowFormatter().removeStyleName(row, HIGHLIGHTED_STYLE);
247    }
248
249    public void sinkRightClickEvents() {
250        table.addContextMenuHandler(this);
251    }
252
253    @Override
254    public void onClick(ClickEvent event) {
255        onCellClicked(event, false);
256    }
257
258    @Override
259    public void onContextMenu(ContextMenuEvent event) {
260        onCellClicked(event, true);
261    }
262
263    private void onCellClicked(DomEvent<?> event, boolean isRightClick) {
264        HTMLTable.Cell tableCell = table.getCellForDomEvent(event);
265        if (tableCell == null) {
266            return;
267        }
268
269        int row = tableCell.getRowIndex();
270        int cell = tableCell.getCellIndex();
271
272        if (isClickableWidgetColumn(cell) && table.getWidget(row, cell) != null) {
273            return;
274        }
275
276        onCellClicked(row, cell, isRightClick);
277    }
278
279    protected void onCellClicked(int row, int cell, boolean isRightClick) {
280        if (row != headerRow) {
281            notifyListenersClicked(row - headerRow - 1, isRightClick);
282        }
283    }
284
285    public void addListener(DataTableListener listener) {
286        listeners.add(listener);
287    }
288
289    public void removeListener(DataTableListener listener) {
290        listeners.remove(listener);
291    }
292
293    protected void notifyListenersClicked(int rowIndex, boolean isRightClick) {
294        JSONObject row = getRow(rowIndex);
295        for (DataTableListener listener : listeners) {
296            listener.onRowClicked(rowIndex, row, isRightClick);
297        }
298    }
299
300    public void refreshWidgets() {
301        for (int row = 1; row < table.getRowCount(); row++) {
302            for (int column = 0; column < columns.length; column++) {
303                if (!isWidgetColumn(column)) {
304                    continue;
305                }
306                table.clearCell(row, column);
307                table.setWidget(row, column, getWidgetForCell(row, column));
308            }
309        }
310    }
311
312    private Widget getWidgetForCell(int row, int column) {
313        return widgetFactory.createWidget(row - 1, column, jsonObjects.get(row - 1));
314    }
315
316    /**
317     * Add a style name to a specific column by column name.
318     */
319    public void addStyleNameByColumnName(String columnName, String styleName) {
320        CellFormatter cellFormatter = table.getCellFormatter();
321        for (int column = 0; column < columns.length; column++) {
322            if (columns[column][1].equals(columnName)) {
323                for (int row = 1; row < table.getRowCount(); row++) {
324                    cellFormatter.addStyleName(row, column, styleName);
325                }
326            }
327        }
328    }
329}
330