Index: frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java |
diff --git a/frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java b/frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..822ac22ec2b04d316b11658cbc894e6302f16899 |
--- /dev/null |
+++ b/frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java |
@@ -0,0 +1,589 @@ |
+package autotest.common.spreadsheet; |
+ |
+import autotest.common.UnmodifiableSublistView; |
+import autotest.common.Utils; |
+import autotest.common.table.FragmentedTable; |
+import autotest.common.table.TableRenderer; |
+import autotest.common.ui.RightClickTable; |
+ |
+import com.google.gwt.dom.client.Element; |
+import com.google.gwt.event.dom.client.ClickEvent; |
+import com.google.gwt.event.dom.client.ClickHandler; |
+import com.google.gwt.event.dom.client.ContextMenuEvent; |
+import com.google.gwt.event.dom.client.ContextMenuHandler; |
+import com.google.gwt.event.dom.client.DomEvent; |
+import com.google.gwt.event.dom.client.ScrollEvent; |
+import com.google.gwt.event.dom.client.ScrollHandler; |
+import com.google.gwt.user.client.DeferredCommand; |
+import com.google.gwt.user.client.IncrementalCommand; |
+import com.google.gwt.user.client.Window; |
+import com.google.gwt.user.client.ui.Composite; |
+import com.google.gwt.user.client.ui.FlexTable; |
+import com.google.gwt.user.client.ui.HTMLTable; |
+import com.google.gwt.user.client.ui.Panel; |
+import com.google.gwt.user.client.ui.ScrollPanel; |
+import com.google.gwt.user.client.ui.SimplePanel; |
+import com.google.gwt.user.client.ui.Widget; |
+ |
+import java.util.ArrayList; |
+import java.util.Collection; |
+import java.util.HashMap; |
+import java.util.List; |
+import java.util.Map; |
+ |
+public class Spreadsheet extends Composite |
+ implements ScrollHandler, ClickHandler, ContextMenuHandler { |
+ |
+ private static final int MIN_TABLE_SIZE_PX = 90; |
+ private static final int WINDOW_BORDER_PX = 15; |
+ private static final int SCROLLBAR_FUDGE = 16; |
+ private static final String BLANK_STRING = "(empty)"; |
+ private static final int CELL_PADDING_PX = 2; |
+ private static final int TD_BORDER_PX = 1; |
+ private static final String HIGHLIGHTED_CLASS = "highlighted"; |
+ private static final int CELLS_PER_ITERATION = 1000; |
+ |
+ private Header rowFields, columnFields; |
+ private List<Header> rowHeaderValues = new ArrayList<Header>(); |
+ private List<Header> columnHeaderValues = new ArrayList<Header>(); |
+ private Map<Header, Integer> rowHeaderMap = new HashMap<Header, Integer>(); |
+ private Map<Header, Integer> columnHeaderMap = new HashMap<Header, Integer>(); |
+ protected CellInfo[][] dataCells, rowHeaderCells, columnHeaderCells; |
+ private RightClickTable rowHeaders = new RightClickTable(); |
+ private RightClickTable columnHeaders = new RightClickTable(); |
+ private FlexTable parentTable = new FlexTable(); |
+ private FragmentedTable dataTable = new FragmentedTable(); |
+ private int rowsPerIteration; |
+ private Panel rowHeadersClipPanel, columnHeadersClipPanel; |
+ private ScrollPanel scrollPanel = new ScrollPanel(dataTable); |
+ private TableRenderer renderer = new TableRenderer(); |
+ |
+ private SpreadsheetListener listener; |
+ |
+ public interface SpreadsheetListener { |
+ public void onCellClicked(CellInfo cellInfo, boolean isRightClick); |
+ } |
+ |
+ public static interface Header extends List<String> {} |
+ public static class HeaderImpl extends ArrayList<String> implements Header { |
+ public HeaderImpl() { |
+ } |
+ |
+ public HeaderImpl(Collection<? extends String> arg0) { |
+ super(arg0); |
+ } |
+ |
+ public static Header fromBaseType(List<String> baseType) { |
+ return new HeaderImpl(baseType); |
+ } |
+ } |
+ |
+ public static class CellInfo { |
+ public Header row, column; |
+ public String contents; |
+ public String color; |
+ public Integer widthPx, heightPx; |
+ public int rowSpan = 1, colSpan = 1; |
+ public int testCount = 0; |
+ public int testIndex; |
+ |
+ public CellInfo(Header row, Header column, String contents) { |
+ this.row = row; |
+ this.column = column; |
+ this.contents = contents; |
+ } |
+ |
+ public boolean isHeader() { |
+ return !isEmpty() && (row == null || column == null); |
+ } |
+ |
+ public boolean isEmpty() { |
+ return row == null && column == null; |
+ } |
+ } |
+ |
+ private class RenderCommand implements IncrementalCommand { |
+ private int state = 0; |
+ private int rowIndex = 0; |
+ private IncrementalCommand onFinished; |
+ |
+ public RenderCommand(IncrementalCommand onFinished) { |
+ this.onFinished = onFinished; |
+ } |
+ |
+ private void renderSomeRows() { |
+ renderer.renderRowsAndAppend(dataTable, dataCells, |
+ rowIndex, rowsPerIteration, true); |
+ rowIndex += rowsPerIteration; |
+ if (rowIndex > dataCells.length) { |
+ state++; |
+ } |
+ } |
+ |
+ public boolean execute() { |
+ switch (state) { |
+ case 0: |
+ computeRowsPerIteration(); |
+ computeHeaderCells(); |
+ break; |
+ case 1: |
+ renderHeaders(); |
+ expandRowHeaders(); |
+ break; |
+ case 2: |
+ // resize everything to the max dimensions (the window size) |
+ fillWindow(false); |
+ break; |
+ case 3: |
+ // set main table to match header sizes |
+ matchRowHeights(rowHeaders, dataCells); |
+ matchColumnWidths(columnHeaders, dataCells); |
+ dataTable.setVisible(false); |
+ break; |
+ case 4: |
+ // render the main data table |
+ renderSomeRows(); |
+ return true; |
+ case 5: |
+ dataTable.updateBodyElems(); |
+ dataTable.setVisible(true); |
+ break; |
+ case 6: |
+ // now expand headers as necessary |
+ // this can be very slow, so put it in it's own cycle |
+ matchRowHeights(dataTable, rowHeaderCells); |
+ break; |
+ case 7: |
+ matchColumnWidths(dataTable, columnHeaderCells); |
+ renderHeaders(); |
+ break; |
+ case 8: |
+ // shrink the scroller if the table ended up smaller than the window |
+ fillWindow(true); |
+ DeferredCommand.addCommand(onFinished); |
+ return false; |
+ } |
+ |
+ state++; |
+ return true; |
+ } |
+ } |
+ |
+ public Spreadsheet() { |
+ dataTable.setStyleName("spreadsheet-data"); |
+ killPaddingAndSpacing(dataTable); |
+ |
+ rowHeaders.setStyleName("spreadsheet-headers"); |
+ killPaddingAndSpacing(rowHeaders); |
+ rowHeadersClipPanel = wrapWithClipper(rowHeaders); |
+ |
+ columnHeaders.setStyleName("spreadsheet-headers"); |
+ killPaddingAndSpacing(columnHeaders); |
+ columnHeadersClipPanel = wrapWithClipper(columnHeaders); |
+ |
+ scrollPanel.setStyleName("spreadsheet-scroller"); |
+ scrollPanel.setAlwaysShowScrollBars(true); |
+ scrollPanel.addScrollHandler(this); |
+ |
+ parentTable.setStyleName("spreadsheet-parent"); |
+ killPaddingAndSpacing(parentTable); |
+ parentTable.setWidget(0, 1, columnHeadersClipPanel); |
+ parentTable.setWidget(1, 0, rowHeadersClipPanel); |
+ parentTable.setWidget(1, 1, scrollPanel); |
+ |
+ setupTableInput(dataTable); |
+ setupTableInput(rowHeaders); |
+ setupTableInput(columnHeaders); |
+ |
+ initWidget(parentTable); |
+ } |
+ |
+ private void setupTableInput(RightClickTable table) { |
+ table.addContextMenuHandler(this); |
+ table.addClickHandler(this); |
+ } |
+ |
+ protected void killPaddingAndSpacing(HTMLTable table) { |
+ table.setCellSpacing(0); |
+ table.setCellPadding(0); |
+ } |
+ |
+ /* |
+ * Wrap a widget with a panel that will clip its contents rather than grow |
+ * too much. |
+ */ |
+ protected Panel wrapWithClipper(Widget w) { |
+ SimplePanel wrapper = new SimplePanel(); |
+ wrapper.add(w); |
+ wrapper.setStyleName("clipper"); |
+ return wrapper; |
+ } |
+ |
+ public void setHeaderFields(Header rowFields, Header columnFields) { |
+ this.rowFields = rowFields; |
+ this.columnFields = columnFields; |
+ } |
+ |
+ private void addHeader(List<Header> headerList, Map<Header, Integer> headerMap, |
+ List<String> header) { |
+ Header headerObject = HeaderImpl.fromBaseType(header); |
+ assert !headerMap.containsKey(headerObject); |
+ headerList.add(headerObject); |
+ headerMap.put(headerObject, headerMap.size()); |
+ } |
+ |
+ public void addRowHeader(List<String> header) { |
+ addHeader(rowHeaderValues, rowHeaderMap, header); |
+ } |
+ |
+ public void addColumnHeader(List<String> header) { |
+ addHeader(columnHeaderValues, columnHeaderMap, header); |
+ } |
+ |
+ private int getHeaderPosition(Map<Header, Integer> headerMap, Header header) { |
+ assert headerMap.containsKey(header); |
+ return headerMap.get(header); |
+ } |
+ |
+ private int getRowPosition(Header rowHeader) { |
+ return getHeaderPosition(rowHeaderMap, rowHeader); |
+ } |
+ |
+ private int getColumnPosition(Header columnHeader) { |
+ return getHeaderPosition(columnHeaderMap, columnHeader); |
+ } |
+ |
+ /** |
+ * Must be called after adding headers but before adding data |
+ */ |
+ public void prepareForData() { |
+ dataCells = new CellInfo[rowHeaderValues.size()][columnHeaderValues.size()]; |
+ } |
+ |
+ public CellInfo getCellInfo(int row, int column) { |
+ Header rowHeader = rowHeaderValues.get(row); |
+ Header columnHeader = columnHeaderValues.get(column); |
+ if (dataCells[row][column] == null) { |
+ dataCells[row][column] = new CellInfo(rowHeader, columnHeader, ""); |
+ } |
+ return dataCells[row][column]; |
+ } |
+ |
+ private CellInfo getCellInfo(CellInfo[][] cells, int row, int column) { |
+ if (cells[row][column] == null) { |
+ cells[row][column] = new CellInfo(null, null, " "); |
+ } |
+ return cells[row][column]; |
+ } |
+ |
+ /** |
+ * Render the data into HTML tables. Done through a deferred command. |
+ */ |
+ public void render(IncrementalCommand onFinished) { |
+ DeferredCommand.addCommand(new RenderCommand(onFinished)); |
+ } |
+ |
+ private void renderHeaders() { |
+ renderer.renderRows(rowHeaders, rowHeaderCells, false); |
+ renderer.renderRows(columnHeaders, columnHeaderCells, false); |
+ } |
+ |
+ public void computeRowsPerIteration() { |
+ int cellsPerRow = columnHeaderValues.size(); |
+ rowsPerIteration = Math.max(CELLS_PER_ITERATION / cellsPerRow, 1); |
+ dataTable.setRowsPerFragment(rowsPerIteration); |
+ } |
+ |
+ private void computeHeaderCells() { |
+ rowHeaderCells = new CellInfo[rowHeaderValues.size()][rowFields.size()]; |
+ fillHeaderCells(rowHeaderCells, rowFields, rowHeaderValues, true); |
+ |
+ columnHeaderCells = new CellInfo[columnFields.size()][columnHeaderValues.size()]; |
+ fillHeaderCells(columnHeaderCells, columnFields, columnHeaderValues, false); |
+ } |
+ |
+ /** |
+ * TODO (post-1.0) - this method needs good cleanup and documentation |
+ */ |
+ private void fillHeaderCells(CellInfo[][] cells, Header fields, List<Header> headerValues, |
+ boolean isRows) { |
+ int headerSize = fields.size(); |
+ String[] lastFieldValue = new String[headerSize]; |
+ CellInfo[] lastCellInfo = new CellInfo[headerSize]; |
+ int[] counter = new int[headerSize]; |
+ boolean newHeader; |
+ for (int headerIndex = 0; headerIndex < headerValues.size(); headerIndex++) { |
+ Header header = headerValues.get(headerIndex); |
+ newHeader = false; |
+ for (int fieldIndex = 0; fieldIndex < headerSize; fieldIndex++) { |
+ String fieldValue = header.get(fieldIndex); |
+ if (newHeader || !fieldValue.equals(lastFieldValue[fieldIndex])) { |
+ newHeader = true; |
+ Header currentHeader = getSubHeader(header, fieldIndex + 1); |
+ String cellContents = formatHeader(fields.get(fieldIndex), fieldValue); |
+ CellInfo cellInfo; |
+ if (isRows) { |
+ cellInfo = new CellInfo(currentHeader, null, cellContents); |
+ cells[headerIndex][fieldIndex] = cellInfo; |
+ } else { |
+ cellInfo = new CellInfo(null, currentHeader, cellContents); |
+ cells[fieldIndex][counter[fieldIndex]] = cellInfo; |
+ counter[fieldIndex]++; |
+ } |
+ lastFieldValue[fieldIndex] = fieldValue; |
+ lastCellInfo[fieldIndex] = cellInfo; |
+ } else { |
+ incrementSpan(lastCellInfo[fieldIndex], isRows); |
+ } |
+ } |
+ } |
+ } |
+ |
+ private String formatHeader(String field, String value) { |
+ if (value.equals("")) { |
+ return BLANK_STRING; |
+ } |
+ value = Utils.escape(value); |
+ if (field.equals("kernel")) { |
+ // line break after each /, for long paths |
+ value = value.replace("/", "/<br>").replace("/<br>/<br>", "//"); |
+ } |
+ return value; |
+ } |
+ |
+ private void incrementSpan(CellInfo cellInfo, boolean isRows) { |
+ if (isRows) { |
+ cellInfo.rowSpan++; |
+ } else { |
+ cellInfo.colSpan++; |
+ } |
+ } |
+ |
+ private Header getSubHeader(Header header, int length) { |
+ if (length == header.size()) { |
+ return header; |
+ } |
+ List<String> subHeader = new UnmodifiableSublistView<String>(header, 0, length); |
+ return new HeaderImpl(subHeader); |
+ } |
+ |
+ private void matchRowHeights(HTMLTable from, CellInfo[][] to) { |
+ int lastColumn = to[0].length - 1; |
+ int rowCount = from.getRowCount(); |
+ for (int row = 0; row < rowCount; row++) { |
+ int height = getRowHeight(from, row); |
+ getCellInfo(to, row, lastColumn).heightPx = height - 2 * CELL_PADDING_PX; |
+ } |
+ } |
+ |
+ private void matchColumnWidths(HTMLTable from, CellInfo[][] to) { |
+ int lastToRow = to.length - 1; |
+ int lastFromRow = from.getRowCount() - 1; |
+ for (int column = 0; column < from.getCellCount(lastFromRow); column++) { |
+ int width = getColumnWidth(from, column); |
+ getCellInfo(to, lastToRow, column).widthPx = width - 2 * CELL_PADDING_PX; |
+ } |
+ } |
+ |
+ protected String getTableCellText(HTMLTable table, int row, int column) { |
+ Element td = table.getCellFormatter().getElement(row, column); |
+ Element div = td.getFirstChildElement(); |
+ if (div == null) |
+ return null; |
+ String contents = Utils.unescape(div.getInnerHTML()); |
+ if (contents.equals(BLANK_STRING)) |
+ contents = ""; |
+ return contents; |
+ } |
+ |
+ public void clear() { |
+ rowHeaderValues.clear(); |
+ columnHeaderValues.clear(); |
+ rowHeaderMap.clear(); |
+ columnHeaderMap.clear(); |
+ dataCells = rowHeaderCells = columnHeaderCells = null; |
+ dataTable.reset(); |
+ |
+ setRowHeadersOffset(0); |
+ setColumnHeadersOffset(0); |
+ } |
+ |
+ /** |
+ * Make the spreadsheet fill the available window space to the right and bottom |
+ * of its position. |
+ */ |
+ public void fillWindow(boolean useTableSize) { |
+ int newHeightPx = Window.getClientHeight() - (columnHeaders.getAbsoluteTop() + |
+ columnHeaders.getOffsetHeight()); |
+ newHeightPx = adjustMaxDimension(newHeightPx); |
+ int newWidthPx = Window.getClientWidth() - (rowHeaders.getAbsoluteLeft() + |
+ rowHeaders.getOffsetWidth()); |
+ newWidthPx = adjustMaxDimension(newWidthPx); |
+ if (useTableSize) { |
+ newHeightPx = Math.min(newHeightPx, rowHeaders.getOffsetHeight()); |
+ newWidthPx = Math.min(newWidthPx, columnHeaders.getOffsetWidth()); |
+ } |
+ |
+ // apply the changes all together |
+ rowHeadersClipPanel.setHeight(getSizePxString(newHeightPx)); |
+ columnHeadersClipPanel.setWidth(getSizePxString(newWidthPx)); |
+ scrollPanel.setSize(getSizePxString(newWidthPx + SCROLLBAR_FUDGE), |
+ getSizePxString(newHeightPx + SCROLLBAR_FUDGE)); |
+ } |
+ |
+ /** |
+ * Adjust a maximum table dimension to allow room for edge decoration and |
+ * always maintain a minimum height |
+ */ |
+ protected int adjustMaxDimension(int maxDimensionPx) { |
+ return Math.max(maxDimensionPx - WINDOW_BORDER_PX - SCROLLBAR_FUDGE, |
+ MIN_TABLE_SIZE_PX); |
+ } |
+ |
+ protected String getSizePxString(int sizePx) { |
+ return sizePx + "px"; |
+ } |
+ |
+ /** |
+ * Ensure the row header clip panel allows the full width of the row headers |
+ * to display. |
+ */ |
+ protected void expandRowHeaders() { |
+ int width = rowHeaders.getOffsetWidth(); |
+ rowHeadersClipPanel.setWidth(getSizePxString(width)); |
+ } |
+ |
+ private Element getCellElement(HTMLTable table, int row, int column) { |
+ return table.getCellFormatter().getElement(row, column); |
+ } |
+ |
+ private Element getCellElement(CellInfo cellInfo) { |
+ assert cellInfo.row != null || cellInfo.column != null; |
+ Element tdElement; |
+ if (cellInfo.row == null) { |
+ tdElement = getCellElement(columnHeaders, 0, getColumnPosition(cellInfo.column)); |
+ } else if (cellInfo.column == null) { |
+ tdElement = getCellElement(rowHeaders, getRowPosition(cellInfo.row), 0); |
+ } else { |
+ tdElement = getCellElement(dataTable, getRowPosition(cellInfo.row), |
+ getColumnPosition(cellInfo.column)); |
+ } |
+ Element cellElement = tdElement.getFirstChildElement(); |
+ assert cellElement != null; |
+ return cellElement; |
+ } |
+ |
+ protected int getColumnWidth(HTMLTable table, int column) { |
+ // using the column formatter doesn't seem to work |
+ int numRows = table.getRowCount(); |
+ return table.getCellFormatter().getElement(numRows - 1, column).getOffsetWidth() - |
+ TD_BORDER_PX; |
+ } |
+ |
+ protected int getRowHeight(HTMLTable table, int row) { |
+ // see getColumnWidth() |
+ int numCols = table.getCellCount(row); |
+ return table.getCellFormatter().getElement(row, numCols - 1).getOffsetHeight() - |
+ TD_BORDER_PX; |
+ } |
+ |
+ /** |
+ * Update floating headers. |
+ */ |
+ @Override |
+ public void onScroll(ScrollEvent event) { |
+ int scrollLeft = scrollPanel.getHorizontalScrollPosition(); |
+ int scrollTop = scrollPanel.getScrollPosition(); |
+ |
+ setColumnHeadersOffset(-scrollLeft); |
+ setRowHeadersOffset(-scrollTop); |
+ } |
+ |
+ protected void setRowHeadersOffset(int offset) { |
+ rowHeaders.getElement().getStyle().setPropertyPx("top", offset); |
+ } |
+ |
+ protected void setColumnHeadersOffset(int offset) { |
+ columnHeaders.getElement().getStyle().setPropertyPx("left", offset); |
+ } |
+ |
+ @Override |
+ public void onClick(ClickEvent event) { |
+ handleEvent(event, false); |
+ } |
+ |
+ @Override |
+ public void onContextMenu(ContextMenuEvent event) { |
+ handleEvent(event, true); |
+ } |
+ |
+ private void handleEvent(DomEvent<?> event, boolean isRightClick) { |
+ if (listener == null) |
+ return; |
+ |
+ assert event.getSource() instanceof RightClickTable; |
+ HTMLTable.Cell tableCell = ((RightClickTable) event.getSource()).getCellForDomEvent(event); |
+ int row = tableCell.getRowIndex(); |
+ int column = tableCell.getCellIndex(); |
+ |
+ CellInfo[][] cells; |
+ if (event.getSource() == rowHeaders) { |
+ cells = rowHeaderCells; |
+ column = adjustRowHeaderColumnIndex(row, column); |
+ } |
+ else if (event.getSource() == columnHeaders) { |
+ cells = columnHeaderCells; |
+ } |
+ else { |
+ assert event.getSource() == dataTable; |
+ cells = dataCells; |
+ } |
+ CellInfo cell = cells[row][column]; |
+ if (cell == null || cell.isEmpty()) |
+ return; // don't report clicks on empty cells |
+ |
+ listener.onCellClicked(cell, isRightClick); |
+ } |
+ |
+ /** |
+ * In HTMLTables, a cell with rowspan > 1 won't count in column indices for the extra rows it |
+ * spans, which will mess up column indices for other cells in those rows. This method adjusts |
+ * the column index passed to onCellClicked() to account for that. |
+ */ |
+ private int adjustRowHeaderColumnIndex(int row, int column) { |
+ for (int i = 0; i < rowFields.size(); i++) { |
+ if (rowHeaderCells[row][i] != null) { |
+ return i + column; |
+ } |
+ } |
+ |
+ throw new RuntimeException("Failed to find non-null cell"); |
+ } |
+ |
+ public void setListener(SpreadsheetListener listener) { |
+ this.listener = listener; |
+ } |
+ |
+ public void setHighlighted(CellInfo cell, boolean highlighted) { |
+ Element cellElement = getCellElement(cell); |
+ if (highlighted) { |
+ cellElement.setClassName(HIGHLIGHTED_CLASS); |
+ } else { |
+ cellElement.setClassName(""); |
+ } |
+ } |
+ |
+ public List<Integer> getAllTestIndices() { |
+ List<Integer> testIndices = new ArrayList<Integer>(); |
+ |
+ for (CellInfo[] row : dataCells) { |
+ for (CellInfo cellInfo : row) { |
+ if (cellInfo != null && !cellInfo.isEmpty()) { |
+ testIndices.add(cellInfo.testIndex); |
+ } |
+ } |
+ } |
+ |
+ return testIndices; |
+ } |
+} |