Home | History | Annotate | Download | only in spreadsheet
      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 {
     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 {
     69         public HeaderImpl() {
     70         }
     71 
     72         public HeaderImpl(Collection<? extends String> arg0) {
     73             super(arg0);
     74         }
     75 
     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 
     90         public CellInfo(Header row, Header column, String contents) {
     91             this.row = row;
     92             this.column = column;
     93             this.contents = contents;
     94         }
     95 
     96         public boolean isHeader() {
     97             return !isEmpty() && (row == null || column == null);
     98         }
     99 
    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 
    110         public RenderCommand(IncrementalCommand onFinished) {
    111             this.onFinished = onFinished;
    112         }
    113 
    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 
    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 
    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 
    201     private void setupTableInput(RightClickTable table) {
    202         table.addContextMenuHandler(this);
    203         table.addClickHandler(this);
    204     }
    205 
    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      */
    215     protected Panel wrapWithClipper(Widget w) {
    216         SimplePanel wrapper = new SimplePanel();
    217         wrapper.add(w);
    218         wrapper.setStyleName("clipper");
    219         return wrapper;
    220     }
    221 
    222     public void setHeaderFields(Header rowFields, Header columnFields) {
    223         this.rowFields = rowFields;
    224         this.columnFields = columnFields;
    225     }
    226 
    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 
    235     public void addRowHeader(List<String> header) {
    236         addHeader(rowHeaderValues, rowHeaderMap, header);
    237     }
    238 
    239     public void addColumnHeader(List<String> header) {
    240         addHeader(columnHeaderValues, columnHeaderMap, header);
    241     }
    242 
    243     private int getHeaderPosition(Map<Header, Integer> headerMap, Header header) {
    244         assert headerMap.containsKey(header);
    245         return headerMap.get(header);
    246     }
    247 
    248     private int getRowPosition(Header rowHeader) {
    249         return getHeaderPosition(rowHeaderMap, rowHeader);
    250     }
    251 
    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      */
    259     public void prepareForData() {
    260         dataCells = new CellInfo[rowHeaderValues.size()][columnHeaderValues.size()];
    261     }
    262 
    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 
    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      */
    282     public void render(IncrementalCommand onFinished) {
    283         DeferredCommand.addCommand(new RenderCommand(onFinished));
    284     }
    285 
    286     private void renderHeaders() {
    287         renderer.renderRows(rowHeaders, rowHeaderCells, false);
    288         renderer.renderRows(columnHeaders, columnHeaderCells, false);
    289     }
    290 
    291     public void computeRowsPerIteration() {
    292         int cellsPerRow = columnHeaderValues.size();
    293         rowsPerIteration = Math.max(CELLS_PER_ITERATION / cellsPerRow, 1);
    294         dataTable.setRowsPerFragment(rowsPerIteration);
    295     }
    296 
    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      */
    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 
    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 
    354     private void incrementSpan(CellInfo cellInfo, boolean isRows) {
    355         if (isRows) {
    356             cellInfo.rowSpan++;
    357         } else {
    358             cellInfo.colSpan++;
    359         }
    360     }
    361 
    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 
    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 
    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 
    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 
    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      */
    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      */
    438     protected int adjustMaxDimension(int maxDimensionPx) {
    439         return Math.max(maxDimensionPx - WINDOW_BORDER_PX - SCROLLBAR_FUDGE,
    440                         MIN_TABLE_SIZE_PX);
    441     }
    442 
    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      */
    451     protected void expandRowHeaders() {
    452         int width = rowHeaders.getOffsetWidth();
    453         rowHeadersClipPanel.setWidth(getSizePxString(width));
    454     }
    455 
    456     private Element getCellElement(HTMLTable table, int row, int column) {
    457         return table.getCellFormatter().getElement(row, column);
    458     }
    459 
    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 
    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 
    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
    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 
    502     protected void setRowHeadersOffset(int offset) {
    503         rowHeaders.getElement().getStyle().setPropertyPx("top", offset);
    504     }
    505 
    506     protected void setColumnHeadersOffset(int offset) {
    507         columnHeaders.getElement().getStyle().setPropertyPx("left", offset);
    508     }
    509 
    510     @Override
    511     public void onClick(ClickEvent event) {
    512         handleEvent(event, false);
    513     }
    514 
    515     @Override
    516     public void onContextMenu(ContextMenuEvent event) {
    517         handleEvent(event, true);
    518     }
    519 
    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      */
    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 
    563     public void setListener(SpreadsheetListener listener) {
    564         this.listener = listener;
    565     }
    566 
    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 
    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