blob: 381c0b69758a3ce346b529afa1fff9940a4f068f [file] [log] [blame]
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 cssClass;
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;
}
}