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