• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package autotest.common.spreadsheet;
2 
3 import autotest.common.UnmodifiableSublistView;
4 import autotest.common.Utils;
5 import autotest.common.table.FragmentedTable;
6 import autotest.common.table.TableRenderer;
7 import autotest.common.ui.RightClickTable;
8 
9 import com.google.gwt.dom.client.Element;
10 import com.google.gwt.event.dom.client.ClickEvent;
11 import com.google.gwt.event.dom.client.ClickHandler;
12 import com.google.gwt.event.dom.client.ContextMenuEvent;
13 import com.google.gwt.event.dom.client.ContextMenuHandler;
14 import com.google.gwt.event.dom.client.DomEvent;
15 import com.google.gwt.event.dom.client.ScrollEvent;
16 import com.google.gwt.event.dom.client.ScrollHandler;
17 import com.google.gwt.user.client.DeferredCommand;
18 import com.google.gwt.user.client.IncrementalCommand;
19 import com.google.gwt.user.client.Window;
20 import com.google.gwt.user.client.ui.Composite;
21 import com.google.gwt.user.client.ui.FlexTable;
22 import com.google.gwt.user.client.ui.HTMLTable;
23 import com.google.gwt.user.client.ui.Panel;
24 import com.google.gwt.user.client.ui.ScrollPanel;
25 import com.google.gwt.user.client.ui.SimplePanel;
26 import com.google.gwt.user.client.ui.Widget;
27 
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 
34 public class Spreadsheet extends Composite
35       implements ScrollHandler, ClickHandler, ContextMenuHandler {
36 
37     private static final int MIN_TABLE_SIZE_PX = 90;
38     private static final int WINDOW_BORDER_PX = 15;
39     private static final int SCROLLBAR_FUDGE = 16;
40     private static final String BLANK_STRING = "(empty)";
41     private static final int CELL_PADDING_PX = 2;
42     private static final int TD_BORDER_PX = 1;
43     private static final String HIGHLIGHTED_CLASS = "highlighted";
44     private static final int CELLS_PER_ITERATION = 1000;
45 
46     private Header rowFields, columnFields;
47     private List<Header> rowHeaderValues = new ArrayList<Header>();
48     private List<Header> columnHeaderValues = new ArrayList<Header>();
49     private Map<Header, Integer> rowHeaderMap = new HashMap<Header, Integer>();
50     private Map<Header, Integer> columnHeaderMap = new HashMap<Header, Integer>();
51     protected CellInfo[][] dataCells, rowHeaderCells, columnHeaderCells;
52     private RightClickTable rowHeaders = new RightClickTable();
53     private RightClickTable columnHeaders = new RightClickTable();
54     private FlexTable parentTable = new FlexTable();
55     private FragmentedTable dataTable = new FragmentedTable();
56     private int rowsPerIteration;
57     private Panel rowHeadersClipPanel, columnHeadersClipPanel;
58     private ScrollPanel scrollPanel = new ScrollPanel(dataTable);
59     private TableRenderer renderer = new TableRenderer();
60 
61     private SpreadsheetListener listener;
62 
63     public interface SpreadsheetListener {
onCellClicked(CellInfo cellInfo, boolean isRightClick)64         public void onCellClicked(CellInfo cellInfo, boolean isRightClick);
65     }
66 
67     public static interface Header extends List<String> {}
68     public static class HeaderImpl extends ArrayList<String> implements Header {
HeaderImpl()69         public HeaderImpl() {
70         }
71 
HeaderImpl(Collection<? extends String> arg0)72         public HeaderImpl(Collection<? extends String> arg0) {
73             super(arg0);
74         }
75 
fromBaseType(List<String> baseType)76         public static Header fromBaseType(List<String> baseType) {
77             return new HeaderImpl(baseType);
78         }
79     }
80 
81     public static class CellInfo {
82         public Header row, column;
83         public String contents;
84         public String cssClass;
85         public Integer widthPx, heightPx;
86         public int rowSpan = 1, colSpan = 1;
87         public int testCount = 0;
88         public int testIndex;
89 
CellInfo(Header row, Header column, String contents)90         public CellInfo(Header row, Header column, String contents) {
91             this.row = row;
92             this.column = column;
93             this.contents = contents;
94         }
95 
isHeader()96         public boolean isHeader() {
97             return !isEmpty() && (row == null || column == null);
98         }
99 
isEmpty()100         public boolean isEmpty() {
101             return row == null && column == null;
102         }
103     }
104 
105     private class RenderCommand implements IncrementalCommand {
106         private int state = 0;
107         private int rowIndex = 0;
108         private IncrementalCommand onFinished;
109 
RenderCommand(IncrementalCommand onFinished)110         public RenderCommand(IncrementalCommand onFinished) {
111             this.onFinished = onFinished;
112         }
113 
renderSomeRows()114         private void renderSomeRows() {
115             renderer.renderRowsAndAppend(dataTable, dataCells,
116                                          rowIndex, rowsPerIteration, true);
117             rowIndex += rowsPerIteration;
118             if (rowIndex > dataCells.length) {
119                 state++;
120             }
121         }
122 
execute()123         public boolean execute() {
124             switch (state) {
125                 case 0:
126                     computeRowsPerIteration();
127                     computeHeaderCells();
128                     break;
129                 case 1:
130                     renderHeaders();
131                     expandRowHeaders();
132                     break;
133                 case 2:
134                     // resize everything to the max dimensions (the window size)
135                     fillWindow(false);
136                     break;
137                 case 3:
138                     // set main table to match header sizes
139                     matchRowHeights(rowHeaders, dataCells);
140                     matchColumnWidths(columnHeaders, dataCells);
141                     dataTable.setVisible(false);
142                     break;
143                 case 4:
144                     // render the main data table
145                     renderSomeRows();
146                     return true;
147                 case 5:
148                     dataTable.updateBodyElems();
149                     dataTable.setVisible(true);
150                     break;
151                 case 6:
152                     // now expand headers as necessary
153                     // this can be very slow, so put it in it's own cycle
154                     matchRowHeights(dataTable, rowHeaderCells);
155                     break;
156                 case 7:
157                     matchColumnWidths(dataTable, columnHeaderCells);
158                     renderHeaders();
159                     break;
160                 case 8:
161                     // shrink the scroller if the table ended up smaller than the window
162                     fillWindow(true);
163                     DeferredCommand.addCommand(onFinished);
164                     return false;
165             }
166 
167             state++;
168             return true;
169         }
170     }
171 
Spreadsheet()172     public Spreadsheet() {
173         dataTable.setStyleName("spreadsheet-data");
174         killPaddingAndSpacing(dataTable);
175 
176         rowHeaders.setStyleName("spreadsheet-headers");
177         killPaddingAndSpacing(rowHeaders);
178         rowHeadersClipPanel = wrapWithClipper(rowHeaders);
179 
180         columnHeaders.setStyleName("spreadsheet-headers");
181         killPaddingAndSpacing(columnHeaders);
182         columnHeadersClipPanel = wrapWithClipper(columnHeaders);
183 
184         scrollPanel.setStyleName("spreadsheet-scroller");
185         scrollPanel.setAlwaysShowScrollBars(true);
186         scrollPanel.addScrollHandler(this);
187 
188         parentTable.setStyleName("spreadsheet-parent");
189         killPaddingAndSpacing(parentTable);
190         parentTable.setWidget(0, 1, columnHeadersClipPanel);
191         parentTable.setWidget(1, 0, rowHeadersClipPanel);
192         parentTable.setWidget(1, 1, scrollPanel);
193 
194         setupTableInput(dataTable);
195         setupTableInput(rowHeaders);
196         setupTableInput(columnHeaders);
197 
198         initWidget(parentTable);
199     }
200 
setupTableInput(RightClickTable table)201     private void setupTableInput(RightClickTable table) {
202         table.addContextMenuHandler(this);
203         table.addClickHandler(this);
204     }
205 
killPaddingAndSpacing(HTMLTable table)206     protected void killPaddingAndSpacing(HTMLTable table) {
207         table.setCellSpacing(0);
208         table.setCellPadding(0);
209     }
210 
211     /*
212      * Wrap a widget with a panel that will clip its contents rather than grow
213      * too much.
214      */
wrapWithClipper(Widget w)215     protected Panel wrapWithClipper(Widget w) {
216         SimplePanel wrapper = new SimplePanel();
217         wrapper.add(w);
218         wrapper.setStyleName("clipper");
219         return wrapper;
220     }
221 
setHeaderFields(Header rowFields, Header columnFields)222     public void setHeaderFields(Header rowFields, Header columnFields) {
223         this.rowFields = rowFields;
224         this.columnFields = columnFields;
225     }
226 
addHeader(List<Header> headerList, Map<Header, Integer> headerMap, List<String> header)227     private void addHeader(List<Header> headerList, Map<Header, Integer> headerMap,
228                           List<String> header) {
229         Header headerObject = HeaderImpl.fromBaseType(header);
230         assert !headerMap.containsKey(headerObject);
231         headerList.add(headerObject);
232         headerMap.put(headerObject, headerMap.size());
233     }
234 
addRowHeader(List<String> header)235     public void addRowHeader(List<String> header) {
236         addHeader(rowHeaderValues, rowHeaderMap, header);
237     }
238 
addColumnHeader(List<String> header)239     public void addColumnHeader(List<String> header) {
240         addHeader(columnHeaderValues, columnHeaderMap, header);
241     }
242 
getHeaderPosition(Map<Header, Integer> headerMap, Header header)243     private int getHeaderPosition(Map<Header, Integer> headerMap, Header header) {
244         assert headerMap.containsKey(header);
245         return headerMap.get(header);
246     }
247 
getRowPosition(Header rowHeader)248     private int getRowPosition(Header rowHeader) {
249         return getHeaderPosition(rowHeaderMap, rowHeader);
250     }
251 
getColumnPosition(Header columnHeader)252     private int getColumnPosition(Header columnHeader) {
253         return getHeaderPosition(columnHeaderMap, columnHeader);
254     }
255 
256     /**
257      * Must be called after adding headers but before adding data
258      */
prepareForData()259     public void prepareForData() {
260         dataCells = new CellInfo[rowHeaderValues.size()][columnHeaderValues.size()];
261     }
262 
getCellInfo(int row, int column)263     public CellInfo getCellInfo(int row, int column) {
264         Header rowHeader = rowHeaderValues.get(row);
265         Header columnHeader = columnHeaderValues.get(column);
266         if (dataCells[row][column] == null) {
267             dataCells[row][column] = new CellInfo(rowHeader, columnHeader, "");
268         }
269         return dataCells[row][column];
270     }
271 
getCellInfo(CellInfo[][] cells, int row, int column)272     private CellInfo getCellInfo(CellInfo[][] cells, int row, int column) {
273         if (cells[row][column] == null) {
274             cells[row][column] = new CellInfo(null, null, " ");
275         }
276         return cells[row][column];
277     }
278 
279     /**
280      * Render the data into HTML tables.  Done through a deferred command.
281      */
render(IncrementalCommand onFinished)282     public void render(IncrementalCommand onFinished) {
283         DeferredCommand.addCommand(new RenderCommand(onFinished));
284     }
285 
renderHeaders()286     private void renderHeaders() {
287         renderer.renderRows(rowHeaders, rowHeaderCells, false);
288         renderer.renderRows(columnHeaders, columnHeaderCells, false);
289     }
290 
computeRowsPerIteration()291     public void computeRowsPerIteration() {
292         int cellsPerRow = columnHeaderValues.size();
293         rowsPerIteration = Math.max(CELLS_PER_ITERATION / cellsPerRow, 1);
294         dataTable.setRowsPerFragment(rowsPerIteration);
295     }
296 
computeHeaderCells()297     private void computeHeaderCells() {
298         rowHeaderCells = new CellInfo[rowHeaderValues.size()][rowFields.size()];
299         fillHeaderCells(rowHeaderCells, rowFields, rowHeaderValues, true);
300 
301         columnHeaderCells = new CellInfo[columnFields.size()][columnHeaderValues.size()];
302         fillHeaderCells(columnHeaderCells, columnFields, columnHeaderValues, false);
303     }
304 
305     /**
306      * TODO (post-1.0) - this method needs good cleanup and documentation
307      */
fillHeaderCells(CellInfo[][] cells, Header fields, List<Header> headerValues, boolean isRows)308     private void fillHeaderCells(CellInfo[][] cells, Header fields, List<Header> headerValues,
309                                  boolean isRows) {
310         int headerSize = fields.size();
311         String[] lastFieldValue = new String[headerSize];
312         CellInfo[] lastCellInfo = new CellInfo[headerSize];
313         int[] counter = new int[headerSize];
314         boolean newHeader;
315         for (int headerIndex = 0; headerIndex < headerValues.size(); headerIndex++) {
316             Header header = headerValues.get(headerIndex);
317             newHeader = false;
318             for (int fieldIndex = 0; fieldIndex < headerSize; fieldIndex++) {
319                 String fieldValue = header.get(fieldIndex);
320                 if (newHeader || !fieldValue.equals(lastFieldValue[fieldIndex])) {
321                     newHeader = true;
322                     Header currentHeader = getSubHeader(header, fieldIndex + 1);
323                     String cellContents = formatHeader(fields.get(fieldIndex), fieldValue);
324                     CellInfo cellInfo;
325                     if (isRows) {
326                         cellInfo = new CellInfo(currentHeader, null, cellContents);
327                         cells[headerIndex][fieldIndex] = cellInfo;
328                     } else {
329                         cellInfo = new CellInfo(null, currentHeader, cellContents);
330                         cells[fieldIndex][counter[fieldIndex]] = cellInfo;
331                         counter[fieldIndex]++;
332                     }
333                     lastFieldValue[fieldIndex] = fieldValue;
334                     lastCellInfo[fieldIndex] = cellInfo;
335                 } else {
336                     incrementSpan(lastCellInfo[fieldIndex], isRows);
337                 }
338             }
339         }
340     }
341 
formatHeader(String field, String value)342     private String formatHeader(String field, String value) {
343         if (value.equals("")) {
344             return BLANK_STRING;
345         }
346         value = Utils.escape(value);
347         if (field.equals("kernel")) {
348             // line break after each /, for long paths
349             value = value.replace("/", "/<br>").replace("/<br>/<br>", "//");
350         }
351         return value;
352     }
353 
incrementSpan(CellInfo cellInfo, boolean isRows)354     private void incrementSpan(CellInfo cellInfo, boolean isRows) {
355         if (isRows) {
356             cellInfo.rowSpan++;
357         } else {
358             cellInfo.colSpan++;
359         }
360     }
361 
getSubHeader(Header header, int length)362     private Header getSubHeader(Header header, int length) {
363         if (length == header.size()) {
364             return header;
365         }
366         List<String> subHeader = new UnmodifiableSublistView<String>(header, 0, length);
367         return new HeaderImpl(subHeader);
368     }
369 
matchRowHeights(HTMLTable from, CellInfo[][] to)370     private void matchRowHeights(HTMLTable from, CellInfo[][] to) {
371         int lastColumn = to[0].length - 1;
372         int rowCount = from.getRowCount();
373         for (int row = 0; row < rowCount; row++) {
374             int height = getRowHeight(from, row);
375             getCellInfo(to, row, lastColumn).heightPx = height - 2 * CELL_PADDING_PX;
376         }
377     }
378 
matchColumnWidths(HTMLTable from, CellInfo[][] to)379     private void matchColumnWidths(HTMLTable from, CellInfo[][] to) {
380         int lastToRow = to.length - 1;
381         int lastFromRow = from.getRowCount() - 1;
382         for (int column = 0; column < from.getCellCount(lastFromRow); column++) {
383             int width = getColumnWidth(from, column);
384             getCellInfo(to, lastToRow, column).widthPx = width - 2 * CELL_PADDING_PX;
385         }
386     }
387 
getTableCellText(HTMLTable table, int row, int column)388     protected String getTableCellText(HTMLTable table, int row, int column) {
389         Element td = table.getCellFormatter().getElement(row, column);
390         Element div = td.getFirstChildElement();
391         if (div == null)
392             return null;
393         String contents = Utils.unescape(div.getInnerHTML());
394         if (contents.equals(BLANK_STRING))
395             contents = "";
396         return contents;
397     }
398 
clear()399     public void clear() {
400         rowHeaderValues.clear();
401         columnHeaderValues.clear();
402         rowHeaderMap.clear();
403         columnHeaderMap.clear();
404         dataCells = rowHeaderCells = columnHeaderCells = null;
405         dataTable.reset();
406 
407         setRowHeadersOffset(0);
408         setColumnHeadersOffset(0);
409     }
410 
411     /**
412      * Make the spreadsheet fill the available window space to the right and bottom
413      * of its position.
414      */
fillWindow(boolean useTableSize)415     public void fillWindow(boolean useTableSize) {
416         int newHeightPx = Window.getClientHeight() - (columnHeaders.getAbsoluteTop() +
417                                                       columnHeaders.getOffsetHeight());
418         newHeightPx = adjustMaxDimension(newHeightPx);
419         int newWidthPx = Window.getClientWidth() - (rowHeaders.getAbsoluteLeft() +
420                                                     rowHeaders.getOffsetWidth());
421         newWidthPx = adjustMaxDimension(newWidthPx);
422         if (useTableSize) {
423             newHeightPx = Math.min(newHeightPx, rowHeaders.getOffsetHeight());
424             newWidthPx = Math.min(newWidthPx, columnHeaders.getOffsetWidth());
425         }
426 
427         // apply the changes all together
428         rowHeadersClipPanel.setHeight(getSizePxString(newHeightPx));
429         columnHeadersClipPanel.setWidth(getSizePxString(newWidthPx));
430         scrollPanel.setSize(getSizePxString(newWidthPx + SCROLLBAR_FUDGE),
431                             getSizePxString(newHeightPx + SCROLLBAR_FUDGE));
432     }
433 
434     /**
435      * Adjust a maximum table dimension to allow room for edge decoration and
436      * always maintain a minimum height
437      */
adjustMaxDimension(int maxDimensionPx)438     protected int adjustMaxDimension(int maxDimensionPx) {
439         return Math.max(maxDimensionPx - WINDOW_BORDER_PX - SCROLLBAR_FUDGE,
440                         MIN_TABLE_SIZE_PX);
441     }
442 
getSizePxString(int sizePx)443     protected String getSizePxString(int sizePx) {
444         return sizePx + "px";
445     }
446 
447     /**
448      * Ensure the row header clip panel allows the full width of the row headers
449      * to display.
450      */
expandRowHeaders()451     protected void expandRowHeaders() {
452         int width = rowHeaders.getOffsetWidth();
453         rowHeadersClipPanel.setWidth(getSizePxString(width));
454     }
455 
getCellElement(HTMLTable table, int row, int column)456     private Element getCellElement(HTMLTable table, int row, int column) {
457         return table.getCellFormatter().getElement(row, column);
458     }
459 
getCellElement(CellInfo cellInfo)460     private Element getCellElement(CellInfo cellInfo) {
461         assert cellInfo.row != null || cellInfo.column != null;
462         Element tdElement;
463         if (cellInfo.row == null) {
464             tdElement = getCellElement(columnHeaders, 0, getColumnPosition(cellInfo.column));
465         } else if (cellInfo.column == null) {
466             tdElement = getCellElement(rowHeaders, getRowPosition(cellInfo.row), 0);
467         } else {
468             tdElement = getCellElement(dataTable, getRowPosition(cellInfo.row),
469                                                   getColumnPosition(cellInfo.column));
470         }
471         Element cellElement = tdElement.getFirstChildElement();
472         assert cellElement != null;
473         return cellElement;
474     }
475 
getColumnWidth(HTMLTable table, int column)476     protected int getColumnWidth(HTMLTable table, int column) {
477         // using the column formatter doesn't seem to work
478         int numRows = table.getRowCount();
479         return table.getCellFormatter().getElement(numRows - 1, column).getOffsetWidth() -
480                TD_BORDER_PX;
481     }
482 
getRowHeight(HTMLTable table, int row)483     protected int getRowHeight(HTMLTable table, int row) {
484         // see getColumnWidth()
485         int numCols = table.getCellCount(row);
486         return table.getCellFormatter().getElement(row, numCols - 1).getOffsetHeight() -
487                TD_BORDER_PX;
488     }
489 
490     /**
491      * Update floating headers.
492      */
493     @Override
onScroll(ScrollEvent event)494     public void onScroll(ScrollEvent event) {
495         int scrollLeft = scrollPanel.getHorizontalScrollPosition();
496         int scrollTop = scrollPanel.getScrollPosition();
497 
498         setColumnHeadersOffset(-scrollLeft);
499         setRowHeadersOffset(-scrollTop);
500     }
501 
setRowHeadersOffset(int offset)502     protected void setRowHeadersOffset(int offset) {
503         rowHeaders.getElement().getStyle().setPropertyPx("top", offset);
504     }
505 
setColumnHeadersOffset(int offset)506     protected void setColumnHeadersOffset(int offset) {
507         columnHeaders.getElement().getStyle().setPropertyPx("left", offset);
508     }
509 
510     @Override
onClick(ClickEvent event)511     public void onClick(ClickEvent event) {
512         handleEvent(event, false);
513     }
514 
515     @Override
onContextMenu(ContextMenuEvent event)516     public void onContextMenu(ContextMenuEvent event) {
517         handleEvent(event, true);
518     }
519 
handleEvent(DomEvent<?> event, boolean isRightClick)520     private void handleEvent(DomEvent<?> event, boolean isRightClick) {
521         if (listener == null)
522             return;
523 
524         assert event.getSource() instanceof RightClickTable;
525         HTMLTable.Cell tableCell = ((RightClickTable) event.getSource()).getCellForDomEvent(event);
526         int row = tableCell.getRowIndex();
527         int column = tableCell.getCellIndex();
528 
529         CellInfo[][] cells;
530         if (event.getSource() == rowHeaders) {
531             cells = rowHeaderCells;
532             column = adjustRowHeaderColumnIndex(row, column);
533         }
534         else if (event.getSource() == columnHeaders) {
535             cells = columnHeaderCells;
536         }
537         else {
538             assert event.getSource() == dataTable;
539             cells = dataCells;
540         }
541         CellInfo cell = cells[row][column];
542         if (cell == null || cell.isEmpty())
543             return; // don't report clicks on empty cells
544 
545         listener.onCellClicked(cell, isRightClick);
546     }
547 
548     /**
549      * In HTMLTables, a cell with rowspan > 1 won't count in column indices for the extra rows it
550      * spans, which will mess up column indices for other cells in those rows.  This method adjusts
551      * the column index passed to onCellClicked() to account for that.
552      */
adjustRowHeaderColumnIndex(int row, int column)553     private int adjustRowHeaderColumnIndex(int row, int column) {
554         for (int i = 0; i < rowFields.size(); i++) {
555             if (rowHeaderCells[row][i] != null) {
556                 return i + column;
557             }
558         }
559 
560         throw new RuntimeException("Failed to find non-null cell");
561     }
562 
setListener(SpreadsheetListener listener)563     public void setListener(SpreadsheetListener listener) {
564         this.listener = listener;
565     }
566 
setHighlighted(CellInfo cell, boolean highlighted)567     public void setHighlighted(CellInfo cell, boolean highlighted) {
568         Element cellElement = getCellElement(cell);
569         if (highlighted) {
570             cellElement.setClassName(HIGHLIGHTED_CLASS);
571         } else {
572             cellElement.setClassName("");
573         }
574     }
575 
getAllTestIndices()576     public List<Integer> getAllTestIndices() {
577         List<Integer> testIndices = new ArrayList<Integer>();
578 
579         for (CellInfo[] row : dataCells) {
580             for (CellInfo cellInfo : row) {
581                 if (cellInfo != null && !cellInfo.isEmpty()) {
582                     testIndices.add(cellInfo.testIndex);
583                 }
584             }
585         }
586 
587         return testIndices;
588     }
589 }
590