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