| 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;
|
| + }
|
| +}
|
|
|