Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(380)

Unified Diff: frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java

Issue 1595019: Merge remote branch 'origin/upstream' into tempbranch (Closed)
Patch Set: Created 10 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698