| Index: frontend/client/src/autotest/tko/Spreadsheet.java
|
| diff --git a/frontend/client/src/autotest/tko/Spreadsheet.java b/frontend/client/src/autotest/tko/Spreadsheet.java
|
| index 4d25130386ae19c5ae8ecbc9f2fae118d4322944..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
|
| --- a/frontend/client/src/autotest/tko/Spreadsheet.java
|
| +++ b/frontend/client/src/autotest/tko/Spreadsheet.java
|
| @@ -1,587 +0,0 @@
|
| -package autotest.tko;
|
| -
|
| -import autotest.common.UnmodifiableSublistView;
|
| -import autotest.common.Utils;
|
| -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;
|
| - }
|
| -}
|
|
|