/* @ITMillApache2LicenseForJavaFiles@ */ package com.itmill.toolkit.terminal.gwt.client.ui; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Vector; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.ScrollListener; import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.Widget; import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection; import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener; import com.itmill.toolkit.terminal.gwt.client.Paintable; import com.itmill.toolkit.terminal.gwt.client.UIDL; import com.itmill.toolkit.terminal.gwt.client.Util; import com.itmill.toolkit.terminal.gwt.client.ui.IScrollTable.IScrollTableBody.IScrollTableRow; /** * Constructor for IScrollTable * * IScrollTable is a FlowPanel having two widgets in it: * TableHead component * * ScrollPanel * * TableHead contains table's header and widgets + logic for resizing, * reordering and hiding columns. * * ScrollPanel contains IScrollTableBody object which handles content. To save * some bandwidth and to improve clients responsiveness with loads of data, in * IScrollTableBody all rows are not necessary rendered. There are "spacers" in * IScrollTableBody to use the exact same space as non-rendered rows would use. * This way we can use seamlessly traditional scrollbars and scrolling to fetch * more rows instead of "paging". * * In IScrollTable we listen to scroll events. On horizontal scrolling we also * update TableHeads scroll position which has its scrollbars hidden. On * vertical scroll events we will check if we are reaching the end of area where * we have rows rendered and * * TODO implement unregistering for child components in Cells */ public class IScrollTable extends Composite implements Table, ScrollListener, ContainerResizedListener { public static final String CLASSNAME = "i-table"; /** * multiple of pagelenght which component will cache when requesting more * rows */ private static final double CACHE_RATE = 2; /** * fraction of pageLenght which can be scrolled without making new request */ private static final double CACHE_REACT_RATE = 1.5; public static final char ALIGN_CENTER = 'c'; public static final char ALIGN_LEFT = 'b'; public static final char ALIGN_RIGHT = 'e'; private int firstRowInViewPort = 0; private int pageLength = 15; private boolean showRowHeaders = false; private String[] columnOrder; private ApplicationConnection client; private String paintableId; private boolean immediate; private int selectMode = Table.SELECT_MODE_NONE; private final HashSet selectedRowKeys = new HashSet(); private boolean initializedAndAttached = false; private final TableHead tHead = new TableHead(); private final ScrollPanel bodyContainer = new ScrollPanel(); private int totalRows; private Set collapsedColumns; private final RowRequestHandler rowRequestHandler; private IScrollTableBody tBody; private String width; private String height; private int firstvisible = 0; private boolean sortAscending; private String sortColumn; private boolean columnReordering; /** * This map contains captions and icon urls for actions like: * "33_c" -> * "Edit" * "33_i" -> "http://dom.com/edit.png" */ private final HashMap actionMap = new HashMap(); private String[] visibleColOrder; private boolean initialContentReceived = false; private Element scrollPositionElement; private final FlowPanel panel; private boolean enabled; private boolean showColHeaders; public IScrollTable() { bodyContainer.addScrollListener(this); bodyContainer.setStyleName(CLASSNAME + "-body"); panel = new FlowPanel(); panel.setStyleName(CLASSNAME); panel.add(tHead); panel.add(bodyContainer); rowRequestHandler = new RowRequestHandler(); initWidget(panel); } public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { if (client.updateComponent(this, uidl, true)) { return; } enabled = !uidl.hasAttribute("disabled"); this.client = client; paintableId = uidl.getStringAttribute("id"); immediate = uidl.getBooleanAttribute("immediate"); final int newTotalRows = uidl.getIntAttribute("totalrows"); if (newTotalRows != totalRows) { totalRows = newTotalRows; if (initializedAndAttached) { tBody.setContainerHeight(); } } pageLength = uidl.getIntAttribute("pagelength"); if (pageLength == 0) { pageLength = totalRows; } firstvisible = uidl.hasVariable("firstvisible") ? uidl .getIntVariable("firstvisible") : 0; showRowHeaders = uidl.getBooleanAttribute("rowheaders"); showColHeaders = uidl.getBooleanAttribute("colheaders"); if (uidl.hasAttribute("width")) { width = uidl.getStringAttribute("width"); } if (uidl.hasAttribute("height")) { height = uidl.getStringAttribute("height"); } if (uidl.hasVariable("sortascending")) { sortAscending = uidl.getBooleanVariable("sortascending"); sortColumn = uidl.getStringVariable("sortcolumn"); } if (uidl.hasVariable("selected")) { final Set selectedKeys = uidl .getStringArrayVariableAsSet("selected"); selectedRowKeys.clear(); for (final Iterator it = selectedKeys.iterator(); it.hasNext();) { selectedRowKeys.add(it.next()); } } if (uidl.hasAttribute("selectmode")) { if (uidl.getStringAttribute("selectmode").equals("multi")) { selectMode = Table.SELECT_MODE_MULTI; } else { selectMode = Table.SELECT_MODE_SINGLE; } } if (uidl.hasVariable("columnorder")) { columnReordering = true; columnOrder = uidl.getStringArrayVariable("columnorder"); } if (uidl.hasVariable("collapsedcolumns")) { tHead.setColumnCollapsingAllowed(true); collapsedColumns = uidl .getStringArrayVariableAsSet("collapsedcolumns"); } else { tHead.setColumnCollapsingAllowed(false); } UIDL rowData = null; for (final Iterator it = uidl.getChildIterator(); it.hasNext();) { final UIDL c = (UIDL) it.next(); if (c.getTag().equals("rows")) { rowData = c; } else if (c.getTag().equals("actions")) { updateActionMap(c); } else if (c.getTag().equals("visiblecolumns")) { updateVisibleColumns(c); } } updateHeader(uidl.getStringArrayAttribute("vcolorder")); if (initializedAndAttached) { updateBody(rowData, uidl.getIntAttribute("firstrow"), uidl .getIntAttribute("rows")); } else { tBody = new IScrollTableBody(); tBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"), uidl.getIntAttribute("rows")); bodyContainer.add(tBody); initialContentReceived = true; if (isAttached()) { sizeInit(); } } hideScrollPositionAnnotation(); } private void updateVisibleColumns(UIDL uidl) { final Iterator it = uidl.getChildIterator(); while (it.hasNext()) { final UIDL col = (UIDL) it.next(); tHead.updateCellFromUIDL(col); } } private void updateActionMap(UIDL c) { final Iterator it = c.getChildIterator(); while (it.hasNext()) { final UIDL action = (UIDL) it.next(); final String key = action.getStringAttribute("key"); final String caption = action.getStringAttribute("caption"); actionMap.put(key + "_c", caption); if (action.hasAttribute("icon")) { // TODO need some uri handling ?? actionMap.put(key + "_i", client.translateToolkitUri(action .getStringAttribute("icon"))); } } } public String getActionCaption(String actionKey) { return (String) actionMap.get(actionKey + "_c"); } public String getActionIcon(String actionKey) { return (String) actionMap.get(actionKey + "_i"); } private void updateHeader(String[] strings) { if (strings == null) { return; } int visibleCols = strings.length; int colIndex = 0; if (showRowHeaders) { tHead.enableColumn("0", colIndex); visibleCols++; visibleColOrder = new String[visibleCols]; visibleColOrder[colIndex] = "0"; colIndex++; } else { visibleColOrder = new String[visibleCols]; tHead.removeCell("0"); } for (int i = 0; i < strings.length; i++) { final String cid = strings[i]; visibleColOrder[colIndex] = cid; tHead.enableColumn(cid, colIndex); colIndex++; } tHead.setVisible(showColHeaders); } /** * @param uidl * which contains row data * @param firstRow * first row in data set * @param reqRows * amount of rows in data set */ private void updateBody(UIDL uidl, int firstRow, int reqRows) { if (uidl == null || reqRows < 1) { // container is empty, remove possibly existing rows if (firstRow < 0) { while (tBody.getLastRendered() > tBody.firstRendered) { tBody.unlinkRow(false); } tBody.unlinkRow(false); } return; } tBody.renderRows(uidl, firstRow, reqRows); final int optimalFirstRow = (int) (firstRowInViewPort - pageLength * CACHE_RATE); while (tBody.getFirstRendered() < optimalFirstRow) { // client.console.log("removing row from start"); tBody.unlinkRow(true); } final int optimalLastRow = (int) (firstRowInViewPort + pageLength + pageLength * CACHE_RATE); while (tBody.getLastRendered() > optimalLastRow) { // client.console.log("removing row from the end"); tBody.unlinkRow(false); } tBody.fixSpacers(); } /** * Gives correct column index for given column key ("cid" in UIDL). * * @param colKey * @return column index of visible columns, -1 if column not visible */ private int getColIndexByKey(String colKey) { // return 0 if asked for rowHeaders if ("0".equals(colKey)) { return 0; } for (int i = 0; i < visibleColOrder.length; i++) { if (visibleColOrder[i].equals(colKey)) { return i; } } return -1; } private boolean isCollapsedColumn(String colKey) { if (collapsedColumns == null) { return false; } if (collapsedColumns.contains(colKey)) { return true; } return false; } private String getColKeyByIndex(int index) { return tHead.getHeaderCell(index).getColKey(); } private void setColWidth(int colIndex, int w) { final HeaderCell cell = tHead.getHeaderCell(colIndex); cell.setWidth(w); tBody.setColWidth(colIndex, w); } private int getColWidth(String colKey) { return tHead.getHeaderCell(colKey).getWidth(); } private IScrollTableRow getRenderedRowByKey(String key) { final Iterator it = tBody.iterator(); IScrollTableRow r = null; while (it.hasNext()) { r = (IScrollTableRow) it.next(); if (r.getKey().equals(key)) { return r; } } return null; } private void reOrderColumn(String columnKey, int newIndex) { final int oldIndex = getColIndexByKey(columnKey); // Change header order tHead.moveCell(oldIndex, newIndex); // Change body order tBody.moveCol(oldIndex, newIndex); /* * Build new columnOrder and update it to server Note that columnOrder * also contains collapsed columns so we cannot directly build it from * cells vector Loop the old columnOrder and append in order to new * array unless on moved columnKey. On new index also put the moved key * i == index on columnOrder, j == index on newOrder */ final String oldKeyOnNewIndex = visibleColOrder[newIndex]; if (showRowHeaders) { newIndex--; // columnOrder don't have rowHeader } // add back hidden rows, for (int i = 0; i < columnOrder.length; i++) { if (columnOrder[i].equals(oldKeyOnNewIndex)) { break; // break loop at target } if (isCollapsedColumn(columnOrder[i])) { newIndex++; } } // finally we can build the new columnOrder for server final String[] newOrder = new String[columnOrder.length]; for (int i = 0, j = 0; j < newOrder.length; i++) { if (j == newIndex) { newOrder[j] = columnKey; j++; } if (i == columnOrder.length) { break; } if (columnOrder[i].equals(columnKey)) { continue; } newOrder[j] = columnOrder[i]; j++; } columnOrder = newOrder; // also update visibleColumnOrder int i = showRowHeaders ? 1 : 0; for (int j = 0; j < newOrder.length; j++) { final String cid = newOrder[j]; if (!isCollapsedColumn(cid)) { visibleColOrder[i++] = cid; } } client.updateVariable(paintableId, "columnorder", columnOrder, false); } protected void onAttach() { super.onAttach(); if (initialContentReceived) { sizeInit(); } } protected void onDetach() { rowRequestHandler.cancel(); super.onDetach(); // ensure that scrollPosElement will be detached if (scrollPositionElement != null) { final Element parent = DOM.getParent(scrollPositionElement); if (parent != null) { DOM.removeChild(parent, scrollPositionElement); } } } /** * Run only once when component is attached and received its initial * content. This function : * Syncs headers and bodys "natural widths and * saves the values. * Sets proper width and height * Makes deferred request * to get some cache rows */ private void sizeInit() { /* * We will use browsers table rendering algorithm to find proper column * widths. If content and header take less space than available, we will * divide extra space relatively to each column which has not width set. * * Overflow pixels are added to last column. * */ Iterator headCells = tHead.iterator(); int i = 0; int totalExplicitColumnsWidths = 0; int total = 0; final int[] widths = new int[tHead.visibleCells.size()]; // first loop: collect natural widths while (headCells.hasNext()) { final HeaderCell hCell = (HeaderCell) headCells.next(); int w; if (hCell.getWidth() > 0) { // server has defined column width explicitly w = hCell.getWidth(); totalExplicitColumnsWidths += w; } else { final int hw = DOM.getElementPropertyInt(hCell.getElement(), "offsetWidth"); final int cw = tBody.getColWidth(i); w = (hw > cw ? hw : cw) + IScrollTableBody.CELL_EXTRA_WIDTH; } widths[i] = w; total += w; i++; } tHead.disableBrowserIntelligence(); if (height == null) { bodyContainer.setHeight((tBody.getRowHeight() * pageLength) + "px"); } else { setHeight(height); iLayout(); } if (width == null) { int w = total; w += getScrollbarWidth(); bodyContainer.setWidth(w + "px"); tHead.setWidth(w + "px"); setWidth(w + "px"); } else { if (width.indexOf("px") > 0) { bodyContainer.setWidth(width); tHead.setWidth(width); setWidth(width); } else if (width.indexOf("%") > 0) { if (!width.equals("100%")) { setWidth(width); } // contained blocks are relative to parents bodyContainer.setWidth("100%"); tHead.setWidth("100%"); } } int availW = tBody.getAvailableWidth(); // Hey IE, are you really sure about this? availW = tBody.getAvailableWidth(); if (availW > total) { // natural size is smaller than available space final int extraSpace = availW - total; final int totalWidthR = total - totalExplicitColumnsWidths; if (totalWidthR > 0) { // now we will share this sum relatively to those without // explicit width headCells = tHead.iterator(); i = 0; HeaderCell hCell; while (headCells.hasNext()) { hCell = (HeaderCell) headCells.next(); if (hCell.getWidth() == -1) { int w = widths[i]; final int newSpace = extraSpace * w / totalWidthR; w += newSpace; widths[i] = w; } i++; } } } else { // bodys size will be more than available and scrollbar will appear } // last loop: set possibly modified values i = 0; headCells = tHead.iterator(); while (headCells.hasNext()) { final HeaderCell hCell = (HeaderCell) headCells.next(); if (hCell.getWidth() == -1) { final int w = widths[i]; setColWidth(i, w); } i++; } if (firstvisible > 0) { bodyContainer .setScrollPosition(firstvisible * tBody.getRowHeight()); firstRowInViewPort = firstvisible; } DeferredCommand.addCommand(new Command() { public void execute() { if (totalRows - 1 > tBody.getLastRendered()) { // fetch cache rows rowRequestHandler .setReqFirstRow(tBody.getLastRendered() + 1); rowRequestHandler .setReqRows((int) (pageLength * CACHE_RATE)); rowRequestHandler.deferRowFetch(1); } } }); initializedAndAttached = true; } public void iLayout() { if (height != null) { if (height.equals("100%")) { /* * We define height in pixels with 100% not to include borders * which is what users usually want. So recalculate pixels via * setHeight. */ setHeight(height); } int contentH = (DOM.getElementPropertyInt(getElement(), "clientHeight") - tHead.getOffsetHeight()); if (contentH < 0) { contentH = 0; } bodyContainer.setHeight(contentH + "px"); } } private int getScrollbarWidth() { return bodyContainer.getOffsetWidth() - DOM.getElementPropertyInt(bodyContainer.getElement(), "clientWidth"); } /** * This method has logic which rows needs to be requested from server when * user scrolls */ public void onScroll(Widget widget, int scrollLeft, int scrollTop) { if (!initializedAndAttached) { return; } if (!enabled) { bodyContainer.setScrollPosition(firstRowInViewPort * tBody.getRowHeight()); return; } rowRequestHandler.cancel(); // fix headers horizontal scrolling tHead.setHorizontalScrollPosition(scrollLeft); firstRowInViewPort = (int) Math.ceil(scrollTop / (double) tBody.getRowHeight()); ApplicationConnection.getConsole().log( "At scrolltop: " + scrollTop + " At row " + firstRowInViewPort); int postLimit = (int) (firstRowInViewPort + pageLength + pageLength * CACHE_REACT_RATE); if (postLimit > totalRows - 1) { postLimit = totalRows - 1; } int preLimit = (int) (firstRowInViewPort - pageLength * CACHE_REACT_RATE); if (preLimit < 0) { preLimit = 0; } final int lastRendered = tBody.getLastRendered(); final int firstRendered = tBody.getFirstRendered(); if (postLimit <= lastRendered && preLimit >= firstRendered) { client.updateVariable(paintableId, "firstvisible", firstRowInViewPort, false); return; // scrolled withing "non-react area" } if (firstRowInViewPort - pageLength * CACHE_RATE > lastRendered || firstRowInViewPort + pageLength + pageLength * CACHE_RATE < firstRendered) { // need a totally new set ApplicationConnection.getConsole().log( "Table: need a totally new set"); rowRequestHandler .setReqFirstRow((int) (firstRowInViewPort - pageLength * CACHE_RATE)); rowRequestHandler .setReqRows((int) (2 * CACHE_RATE * pageLength + pageLength)); rowRequestHandler.deferRowFetch(); return; } if (preLimit < firstRendered) { // need some rows to the beginning of the rendered area ApplicationConnection .getConsole() .log( "Table: need some rows to the beginning of the rendered area"); rowRequestHandler .setReqFirstRow((int) (firstRowInViewPort - pageLength * CACHE_RATE)); rowRequestHandler.setReqRows(firstRendered - rowRequestHandler.getReqFirstRow()); rowRequestHandler.deferRowFetch(); return; } if (postLimit > lastRendered) { // need some rows to the end of the rendered area ApplicationConnection.getConsole().log( "need some rows to the end of the rendered area"); rowRequestHandler.setReqFirstRow(lastRendered + 1); rowRequestHandler.setReqRows((int) ((firstRowInViewPort + pageLength + pageLength * CACHE_RATE) - lastRendered)); rowRequestHandler.deferRowFetch(); } } private void announceScrollPosition() { ApplicationConnection.getConsole().log("" + firstRowInViewPort); if (scrollPositionElement == null) { scrollPositionElement = DOM.createDiv(); DOM.setElementProperty(scrollPositionElement, "className", "i-table-scrollposition"); DOM.appendChild(getElement(), scrollPositionElement); } DOM.setStyleAttribute(scrollPositionElement, "position", "absolute"); DOM.setStyleAttribute(scrollPositionElement, "marginLeft", (DOM .getElementPropertyInt(getElement(), "offsetWidth") / 2 - 80) + "px"); DOM.setStyleAttribute(scrollPositionElement, "marginTop", -(DOM .getElementPropertyInt(getElement(), "offsetHeight") / 2) + "px"); int last = (firstRowInViewPort + pageLength); if (last > totalRows) { last = totalRows; } DOM.setInnerHTML(scrollPositionElement, "" + firstRowInViewPort + " – " + last + "..." + ""); DOM.setStyleAttribute(scrollPositionElement, "display", "block"); } private void hideScrollPositionAnnotation() { if (scrollPositionElement != null) { DOM.setStyleAttribute(scrollPositionElement, "display", "none"); } } private class RowRequestHandler extends Timer { private int reqFirstRow = 0; private int reqRows = 0; public void deferRowFetch() { deferRowFetch(250); } public void deferRowFetch(int msec) { if (reqRows > 0 && reqFirstRow < totalRows) { schedule(msec); // tell scroll position to user if currently "visible" rows are // not rendered if ((firstRowInViewPort + pageLength > tBody.getLastRendered()) || (firstRowInViewPort < tBody.getFirstRendered())) { announceScrollPosition(); } else { hideScrollPositionAnnotation(); } } } public void setReqFirstRow(int reqFirstRow) { if (reqFirstRow < 0) { reqFirstRow = 0; } else if (reqFirstRow >= totalRows) { reqFirstRow = totalRows - 1; } this.reqFirstRow = reqFirstRow; } public void setReqRows(int reqRows) { this.reqRows = reqRows; } public void run() { ApplicationConnection.getConsole().log( "Getting " + reqRows + " rows from " + reqFirstRow); client.updateVariable(paintableId, "firstvisible", firstRowInViewPort, false); client.updateVariable(paintableId, "reqfirstrow", reqFirstRow, false); client.updateVariable(paintableId, "reqrows", reqRows, true); } public int getReqFirstRow() { return reqFirstRow; } public int getReqRows() { return reqRows; } /** * Sends request to refresh content at this position. */ public void refreshContent() { int first = (int) (firstRowInViewPort - pageLength * CACHE_RATE); int reqRows = (int) (2 * pageLength * CACHE_RATE + pageLength); if (first < 0) { reqRows = reqRows + first; first = 0; } setReqFirstRow(first); setReqRows(reqRows); run(); } } public class HeaderCell extends Widget { private static final int DRAG_WIDGET_WIDTH = 4; private static final int MINIMUM_COL_WIDTH = 20; Element td = DOM.createTD(); Element captionContainer = DOM.createDiv(); Element colResizeWidget = DOM.createDiv(); Element floatingCopyOfHeaderCell; private boolean sortable = false; private final String cid; private boolean dragging; private int dragStartX; private int colIndex; private int originalWidth; private boolean isResizing; private int headerX; private boolean moved; private int closestSlot; private int width = -1; private char align = ALIGN_LEFT; public void setSortable(boolean b) { sortable = b; } public HeaderCell(String colId, String headerText) { cid = colId; DOM.setElementProperty(colResizeWidget, "className", CLASSNAME + "-resizer"); DOM.setStyleAttribute(colResizeWidget, "width", DRAG_WIDGET_WIDTH + "px"); DOM.sinkEvents(colResizeWidget, Event.MOUSEEVENTS); setText(headerText); DOM.appendChild(td, colResizeWidget); DOM.setElementProperty(captionContainer, "className", CLASSNAME + "-caption-container"); DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS); DOM.appendChild(td, captionContainer); DOM.sinkEvents(td, Event.MOUSEEVENTS); setElement(td); } public void setWidth(int w) { width = w; DOM.setStyleAttribute(captionContainer, "width", (w - DRAG_WIDGET_WIDTH - 4) + "px"); setWidth(w + "px"); } public int getWidth() { return width; } public void setText(String headerText) { DOM.setInnerHTML(captionContainer, headerText); } public String getColKey() { return cid; } private void setSorted(boolean sorted) { if (sorted) { if (sortAscending) { this.setStyleName(CLASSNAME + "-header-cell-asc"); } else { this.setStyleName(CLASSNAME + "-header-cell-desc"); } } else { this.setStyleName(CLASSNAME + "-header-cell"); } } /** * Handle column reordering. */ public void onBrowserEvent(Event event) { if (enabled) { if (isResizing || DOM.compare(DOM.eventGetTarget(event), colResizeWidget)) { onResizeEvent(event); } else { handleCaptionEvent(event); } } } private void createFloatingCopy() { floatingCopyOfHeaderCell = DOM.createDiv(); DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td)); floatingCopyOfHeaderCell = DOM .getChild(floatingCopyOfHeaderCell, 1); DOM.setElementProperty(floatingCopyOfHeaderCell, "className", CLASSNAME + "-header-drag"); updateFloatingCopysPosition(DOM.getAbsoluteLeft(td), DOM .getAbsoluteTop(td)); DOM.appendChild(RootPanel.get().getElement(), floatingCopyOfHeaderCell); } private void updateFloatingCopysPosition(int x, int y) { x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell, "offsetWidth") / 2; DOM.setStyleAttribute(floatingCopyOfHeaderCell, "left", x + "px"); if (y > 0) { DOM.setStyleAttribute(floatingCopyOfHeaderCell, "top", (y + 7) + "px"); } } private void hideFloatingCopy() { DOM.removeChild(RootPanel.get().getElement(), floatingCopyOfHeaderCell); floatingCopyOfHeaderCell = null; } protected void handleCaptionEvent(Event event) { switch (DOM.eventGetType(event)) { case Event.ONMOUSEDOWN: ApplicationConnection.getConsole().log( "HeaderCaption: mouse down"); if (columnReordering) { dragging = true; moved = false; colIndex = getColIndexByKey(cid); DOM.setCapture(getElement()); headerX = tHead.getAbsoluteLeft(); ApplicationConnection .getConsole() .log( "HeaderCaption: Caption set to capture mouse events"); DOM.eventPreventDefault(event); // prevent selecting text } break; case Event.ONMOUSEUP: ApplicationConnection.getConsole() .log("HeaderCaption: mouseUP"); if (columnReordering) { dragging = false; DOM.releaseCapture(getElement()); ApplicationConnection.getConsole().log( "HeaderCaption: Stopped column reordering"); if (moved) { hideFloatingCopy(); tHead.removeSlotFocus(); if (closestSlot != colIndex && closestSlot != (colIndex + 1)) { if (closestSlot > colIndex) { reOrderColumn(cid, closestSlot - 1); } else { reOrderColumn(cid, closestSlot); } } } } if (!moved) { // mouse event was a click to header -> sort column if (sortable) { if (sortColumn.equals(cid)) { // just toggle order client.updateVariable(paintableId, "sortascending", !sortAscending, false); } else { // set table scrolled by this column client.updateVariable(paintableId, "sortcolumn", cid, false); } // get also cache columns at the same request bodyContainer.setScrollPosition(0); firstvisible = 0; rowRequestHandler.setReqFirstRow(0); rowRequestHandler.setReqRows((int) (2 * pageLength * CACHE_RATE + pageLength)); rowRequestHandler.deferRowFetch(); } break; } break; case Event.ONMOUSEMOVE: if (dragging) { ApplicationConnection.getConsole().log( "HeaderCaption: Dragging column, optimal index..."); if (!moved) { createFloatingCopy(); moved = true; } final int x = DOM.eventGetClientX(event) + DOM.getElementPropertyInt(tHead.hTableWrapper, "scrollLeft"); int slotX = headerX; closestSlot = colIndex; int closestDistance = -1; int start = 0; if (showRowHeaders) { start++; } final int visibleCellCount = tHead.getVisibleCellCount(); for (int i = start; i <= visibleCellCount; i++) { if (i > 0) { final String colKey = getColKeyByIndex(i - 1); slotX += getColWidth(colKey); } final int dist = Math.abs(x - slotX); if (closestDistance == -1 || dist < closestDistance) { closestDistance = dist; closestSlot = i; } } tHead.focusSlot(closestSlot); updateFloatingCopysPosition(DOM.eventGetClientX(event), -1); ApplicationConnection.getConsole().log("" + closestSlot); } break; default: break; } } private void onResizeEvent(Event event) { switch (DOM.eventGetType(event)) { case Event.ONMOUSEDOWN: isResizing = true; DOM.setCapture(getElement()); dragStartX = DOM.eventGetClientX(event); colIndex = getColIndexByKey(cid); originalWidth = getWidth(); DOM.eventPreventDefault(event); break; case Event.ONMOUSEUP: isResizing = false; DOM.releaseCapture(getElement()); break; case Event.ONMOUSEMOVE: if (isResizing) { final int deltaX = DOM.eventGetClientX(event) - dragStartX; if (deltaX == 0) { return; } int newWidth = originalWidth + deltaX; if (newWidth < MINIMUM_COL_WIDTH) { newWidth = MINIMUM_COL_WIDTH; } setColWidth(colIndex, newWidth); } break; default: break; } } public String getCaption() { return DOM.getInnerText(captionContainer); } public boolean isEnabled() { return getParent() != null; } public void setAlign(char c) { if (align != c) { switch (c) { case ALIGN_CENTER: DOM.setStyleAttribute(captionContainer, "textAlign", "center"); break; case ALIGN_RIGHT: DOM.setStyleAttribute(captionContainer, "textAlign", "right"); break; default: DOM.setStyleAttribute(captionContainer, "textAlign", ""); break; } } align = c; } public char getAlign() { return align; } } /** * HeaderCell that is header cell for row headers. * * Reordering disabled and clicking on it resets sorting. */ public class RowHeadersHeaderCell extends HeaderCell { RowHeadersHeaderCell() { super("0", ""); } protected void handleCaptionEvent(Event event) { // NOP: RowHeaders cannot be reordered // TODO It'd be nice to reset sorting here } } public class TableHead extends Panel implements ActionOwner { private static final int WRAPPER_WIDTH = 9000; Vector visibleCells = new Vector(); HashMap availableCells = new HashMap(); Element div = DOM.createDiv(); Element hTableWrapper = DOM.createDiv(); Element hTableContainer = DOM.createDiv(); Element table = DOM.createTable(); Element headerTableBody = DOM.createTBody(); Element tr = DOM.createTR(); private final Element columnSelector = DOM.createDiv(); private int focusedSlot = -1; private boolean columnCollapsing = false; public TableHead() { DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden"); DOM.setElementProperty(hTableWrapper, "className", CLASSNAME + "-header"); // TODO move styles to CSS DOM.setElementProperty(columnSelector, "className", CLASSNAME + "-column-selector"); DOM.setStyleAttribute(columnSelector, "display", "none"); DOM.appendChild(table, headerTableBody); DOM.appendChild(headerTableBody, tr); DOM.appendChild(hTableContainer, table); DOM.appendChild(hTableWrapper, hTableContainer); DOM.appendChild(div, hTableWrapper); DOM.appendChild(div, columnSelector); setElement(div); setStyleName(CLASSNAME + "-header-wrap"); DOM.sinkEvents(columnSelector, Event.ONCLICK); availableCells.put("0", new RowHeadersHeaderCell()); } public void updateCellFromUIDL(UIDL col) { final String cid = col.getStringAttribute("cid"); HeaderCell c = getHeaderCell(cid); if (c == null) { c = new HeaderCell(cid, col.getStringAttribute("caption")); availableCells.put(cid, c); } else { c.setText(col.getStringAttribute("caption")); } if (col.hasAttribute("sortable")) { c.setSortable(true); if (cid.equals(sortColumn)) { c.setSorted(true); } else { c.setSorted(false); } } if (col.hasAttribute("align")) { c.setAlign(col.getStringAttribute("align").charAt(0)); } if (col.hasAttribute("width")) { final String width = col.getStringAttribute("width"); c.setWidth(Integer.parseInt(width)); } // TODO icon } public void enableColumn(String cid, int index) { final HeaderCell c = getHeaderCell(cid); if (!c.isEnabled()) { setHeaderCell(index, c); } } public int getVisibleCellCount() { return visibleCells.size(); } public void setHorizontalScrollPosition(int scrollLeft) { DOM.setElementPropertyInt(hTableWrapper, "scrollLeft", scrollLeft); } public void setColumnCollapsingAllowed(boolean cc) { columnCollapsing = cc; if (cc) { DOM.setStyleAttribute(columnSelector, "display", "block"); } else { DOM.setStyleAttribute(columnSelector, "display", "none"); } } public void disableBrowserIntelligence() { DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH + "px"); } public void setHeaderCell(int index, HeaderCell cell) { if (index < visibleCells.size()) { // insert to right slot DOM.insertChild(tr, cell.getElement(), index); adopt(cell); visibleCells.insertElementAt(cell, index); } else if (index == visibleCells.size()) { // simply append DOM.appendChild(tr, cell.getElement()); adopt(cell); visibleCells.add(cell); } else { throw new RuntimeException( "Header cells must be appended in order"); } } public HeaderCell getHeaderCell(int index) { if (index < visibleCells.size()) { return (HeaderCell) visibleCells.get(index); } else { return null; } } /** * Get's HeaderCell by it's column Key. * * Note that this returns HeaderCell even if it is currently collapsed. * * @param cid * Column key of accessed HeaderCell * @return HeaderCell */ public HeaderCell getHeaderCell(String cid) { return (HeaderCell) availableCells.get(cid); } public void moveCell(int oldIndex, int newIndex) { final HeaderCell hCell = getHeaderCell(oldIndex); final Element cell = hCell.getElement(); visibleCells.remove(oldIndex); DOM.removeChild(tr, cell); DOM.insertChild(tr, cell, newIndex); visibleCells.insertElementAt(hCell, newIndex); } public Iterator iterator() { return visibleCells.iterator(); } public boolean remove(Widget w) { if (visibleCells.contains(w)) { visibleCells.remove(w); orphan(w); DOM.removeChild(DOM.getParent(w.getElement()), w.getElement()); return true; } return false; } public void removeCell(String colKey) { final HeaderCell c = getHeaderCell(colKey); remove(c); } private void focusSlot(int index) { removeSlotFocus(); if (index > 0) { DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr, index - 1)), "className", CLASSNAME + "-resizer " + CLASSNAME + "-focus-slot-right"); } else { DOM.setElementProperty(DOM.getFirstChild(DOM .getChild(tr, index)), "className", CLASSNAME + "-resizer " + CLASSNAME + "-focus-slot-left"); } focusedSlot = index; } private void removeSlotFocus() { if (focusedSlot < 0) { return; } if (focusedSlot == 0) { DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr, focusedSlot)), "className", CLASSNAME + "-resizer"); } else if (focusedSlot > 0) { DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr, focusedSlot - 1)), "className", CLASSNAME + "-resizer"); } focusedSlot = -1; } public void onBrowserEvent(Event event) { if (enabled) { if (DOM.compare(DOM.eventGetTarget(event), columnSelector)) { final int left = DOM.getAbsoluteLeft(columnSelector); final int top = DOM.getAbsoluteTop(columnSelector) + DOM.getElementPropertyInt(columnSelector, "offsetHeight"); client.getContextMenu().showAt(this, left, top); } } } class VisibleColumnAction extends Action { String colKey; private boolean collapsed; public VisibleColumnAction(String colKey) { super(IScrollTable.TableHead.this); this.colKey = colKey; caption = tHead.getHeaderCell(colKey).getCaption(); } public void execute() { client.getContextMenu().hide(); // toggle selected column if (collapsedColumns.contains(colKey)) { collapsedColumns.remove(colKey); } else { tHead.removeCell(colKey); collapsedColumns.add(colKey); } // update variable to server client.updateVariable(paintableId, "collapsedcolumns", collapsedColumns.toArray(), false); // let rowRequestHandler determine proper rows rowRequestHandler.refreshContent(); } public void setCollapsed(boolean b) { collapsed = b; } /** * Override default method to distinguish on/off columns */ public String getHTML() { final StringBuffer buf = new StringBuffer(); if (collapsed) { buf.append(""); } buf.append(super.getHTML()); if (collapsed) { buf.append(""); } return buf.toString(); } } /* * Returns columns as Action array for column select popup */ public Action[] getActions() { Object[] cols; if (columnReordering) { cols = columnOrder; } else { // if columnReordering is disabled, we need different way to get // all available columns cols = visibleColOrder; cols = new Object[visibleColOrder.length + collapsedColumns.size()]; int i; for (i = 0; i < visibleColOrder.length; i++) { cols[i] = visibleColOrder[i]; } for (final Iterator it = collapsedColumns.iterator(); it .hasNext();) { cols[i++] = it.next(); } } final Action[] actions = new Action[cols.length]; for (int i = 0; i < cols.length; i++) { final String cid = (String) cols[i]; final HeaderCell c = getHeaderCell(cid); final VisibleColumnAction a = new VisibleColumnAction(c .getColKey()); a.setCaption(c.getCaption()); if (!c.isEnabled()) { a.setCollapsed(true); } actions[i] = a; } return actions; } public ApplicationConnection getClient() { return client; } public String getPaintableId() { return paintableId; } /** * Returns column alignments for visible columns */ public char[] getColumnAlignments() { final Iterator it = visibleCells.iterator(); final char[] aligns = new char[visibleCells.size()]; int colIndex = 0; while (it.hasNext()) { aligns[colIndex++] = ((HeaderCell) it.next()).getAlign(); } return aligns; } } /** * This Panel can only contain IScrollTableRow type of widgets. This * "simulates" very large table, keeping spacers which take room of * unrendered rows. * */ public class IScrollTableBody extends Panel { public static final int CELL_EXTRA_WIDTH = 20; public static final int DEFAULT_ROW_HEIGHT = 24; public static final int CELL_CONTENT_PADDING = 3; private int rowHeight = -1; private final List renderedRows = new Vector(); private boolean initDone = false; Element preSpacer = DOM.createDiv(); Element postSpacer = DOM.createDiv(); Element container = DOM.createDiv(); Element tBody = DOM.createTBody(); Element table = DOM.createTable(); private int firstRendered; private int lastRendered; private char[] aligns; IScrollTableBody() { constructDOM(); setElement(container); } private void constructDOM() { DOM.setElementProperty(table, "className", CLASSNAME + "-table"); DOM.setElementProperty(preSpacer, "className", CLASSNAME + "-row-spacer"); DOM.setElementProperty(postSpacer, "className", CLASSNAME + "-row-spacer"); DOM.appendChild(table, tBody); DOM.appendChild(container, preSpacer); DOM.appendChild(container, table); DOM.appendChild(container, postSpacer); } public int getAvailableWidth() { return DOM.getElementPropertyInt(preSpacer, "offsetWidth"); } public void renderInitialRows(UIDL rowData, int firstIndex, int rows) { firstRendered = firstIndex; lastRendered = firstIndex + rows - 1; final Iterator it = rowData.getChildIterator(); aligns = tHead.getColumnAlignments(); while (it.hasNext()) { final IScrollTableRow row = new IScrollTableRow((UIDL) it .next(), aligns); addRow(row); } if (isAttached()) { fixSpacers(); } } public void renderRows(UIDL rowData, int firstIndex, int rows) { aligns = tHead.getColumnAlignments(); final Iterator it = rowData.getChildIterator(); if (firstIndex == lastRendered + 1) { while (it.hasNext()) { final IScrollTableRow row = createRow((UIDL) it.next()); addRow(row); lastRendered++; } fixSpacers(); } else if (firstIndex + rows == firstRendered) { final IScrollTableRow[] rowArray = new IScrollTableRow[rows]; int i = rows; while (it.hasNext()) { i--; rowArray[i] = createRow((UIDL) it.next()); } for (i = 0; i < rows; i++) { addRowBeforeFirstRendered(rowArray[i]); firstRendered--; } // } else if (firstIndex > lastRendered || firstIndex + rows < // firstRendered) { } else if (true) { // completely new set of rows while (lastRendered + 1 > firstRendered) { unlinkRow(false); } final IScrollTableRow row = createRow((UIDL) it.next()); firstRendered = firstIndex; lastRendered = firstIndex - 1; addRow(row); lastRendered++; setContainerHeight(); fixSpacers(); while (it.hasNext()) { addRow(createRow((UIDL) it.next())); lastRendered++; } fixSpacers(); } else { // sorted or column reordering changed ApplicationConnection.getConsole().log( "Bad update" + firstIndex + "/" + rows); } } /** * This method is used to instantiate new rows for this table. It * automatically sets correct widths to rows cells and assigns correct * client reference for child widgets. * * This method can be called only after table has been initialized * * @param uidl */ private IScrollTableRow createRow(UIDL uidl) { final IScrollTableRow row = new IScrollTableRow(uidl, aligns); final int cells = DOM.getChildCount(row.getElement()); for (int i = 0; i < cells; i++) { final Element cell = DOM.getChild(row.getElement(), i); final int w = IScrollTable.this .getColWidth(getColKeyByIndex(i)); DOM.setStyleAttribute(DOM.getFirstChild(cell), "width", (w - CELL_CONTENT_PADDING) + "px"); DOM.setStyleAttribute(cell, "width", w + "px"); } return row; } private void addRowBeforeFirstRendered(IScrollTableRow row) { IScrollTableRow first = null; if (renderedRows.size() > 0) { first = (IScrollTableRow) renderedRows.get(0); } if (first != null && first.getStyleName().indexOf("-odd") == -1) { row.setStyleName(CLASSNAME + "-row-odd"); } if (row.isSelected()) { row.addStyleName("i-selected"); } DOM.insertChild(tBody, row.getElement(), 0); adopt(row); renderedRows.add(0, row); } private void addRow(IScrollTableRow row) { IScrollTableRow last = null; if (renderedRows.size() > 0) { last = (IScrollTableRow) renderedRows .get(renderedRows.size() - 1); } if (last != null && last.getStyleName().indexOf("-odd") == -1) { row.setStyleName(CLASSNAME + "-row-odd"); } if (row.isSelected()) { row.addStyleName("i-selected"); } DOM.appendChild(tBody, row.getElement()); adopt(row); renderedRows.add(row); } public Iterator iterator() { return renderedRows.iterator(); } public void unlinkRow(boolean fromBeginning) { if (lastRendered - firstRendered < 0) { return; } int index; if (fromBeginning) { index = 0; firstRendered++; } else { index = renderedRows.size() - 1; lastRendered--; } final IScrollTableRow toBeRemoved = (IScrollTableRow) renderedRows .get(index); client.unregisterChildPaintables(toBeRemoved); DOM.removeChild(tBody, toBeRemoved.getElement()); orphan(toBeRemoved); renderedRows.remove(index); fixSpacers(); } public boolean remove(Widget w) { throw new UnsupportedOperationException(); } protected void onAttach() { super.onAttach(); setContainerHeight(); } /** * Fix container blocks height according to totalRows to avoid * "bouncing" when scrolling */ private void setContainerHeight() { fixSpacers(); DOM.setStyleAttribute(container, "height", totalRows * getRowHeight() + "px"); } private void fixSpacers() { int prepx = getRowHeight() * firstRendered; if (prepx < 0) { prepx = 0; } DOM.setStyleAttribute(preSpacer, "height", prepx + "px"); int postpx = getRowHeight() * (totalRows - 1 - lastRendered); if (postpx < 0) { postpx = 0; } DOM.setStyleAttribute(postSpacer, "height", postpx + "px"); } public int getRowHeight() { if (initDone) { return rowHeight; } else { if (DOM.getChildCount(tBody) > 0) { rowHeight = DOM .getElementPropertyInt(tBody, "offsetHeight") / DOM.getChildCount(tBody); } else { return DEFAULT_ROW_HEIGHT; } initDone = true; return rowHeight; } } public int getColWidth(int i) { if (initDone) { final Element e = DOM.getChild(DOM.getChild(tBody, 0), i); return DOM.getElementPropertyInt(e, "offsetWidth"); } else { return 0; } } public void setColWidth(int colIndex, int w) { final int rows = DOM.getChildCount(tBody); for (int i = 0; i < rows; i++) { final Element cell = DOM.getChild(DOM.getChild(tBody, i), colIndex); DOM.setStyleAttribute(DOM.getFirstChild(cell), "width", (w - CELL_CONTENT_PADDING) + "px"); DOM.setStyleAttribute(cell, "width", w + "px"); } } public int getLastRendered() { return lastRendered; } public int getFirstRendered() { return firstRendered; } public void moveCol(int oldIndex, int newIndex) { // loop all rows and move given index to its new place final Iterator rows = iterator(); while (rows.hasNext()) { final IScrollTableRow row = (IScrollTableRow) rows.next(); final Element td = DOM.getChild(row.getElement(), oldIndex); DOM.removeChild(row.getElement(), td); DOM.insertChild(row.getElement(), td, newIndex); } } public class IScrollTableRow extends Panel implements ActionOwner { Vector childWidgets = new Vector(); private boolean selected = false; private final int rowKey; private String[] actionKeys = null; private IScrollTableRow(int rowKey) { this.rowKey = rowKey; setElement(DOM.createElement("tr")); DOM.sinkEvents(getElement(), Event.ONCLICK); attachContextMenuEvent(getElement()); setStyleName(CLASSNAME + "-row"); } protected void onDetach() { Util.removeContextMenuEvent(getElement()); super.onDetach(); } /** * Attaches context menu event handler to given element. Attached * handler fires showContextMenu function. * * @param el * element where to attach contenxt menu event */ private native void attachContextMenuEvent(Element el) /*-{ var row = this; el.oncontextmenu = function(e) { if(!e) e = $wnd.event; row.@com.itmill.toolkit.terminal.gwt.client.ui.IScrollTable.IScrollTableBody.IScrollTableRow::showContextMenu(Lcom/google/gwt/user/client/Event;)(e); return false; }; }-*/; public String getKey() { return String.valueOf(rowKey); } public IScrollTableRow(UIDL uidl, char[] aligns) { this(uidl.getIntAttribute("key")); tHead.getColumnAlignments(); int col = 0; // row header if (showRowHeaders) { addCell(uidl.getStringAttribute("caption"), aligns[col++]); } if (uidl.hasAttribute("al")) { actionKeys = uidl.getStringArrayAttribute("al"); } final Iterator cells = uidl.getChildIterator(); while (cells.hasNext()) { final Object cell = cells.next(); if (cell instanceof String) { addCell(cell.toString(), aligns[col++]); } else { final Widget cellContent = client .getWidget((UIDL) cell); ((Paintable) cellContent).updateFromUIDL((UIDL) cell, client); addCell(cellContent, aligns[col++]); } } if (uidl.hasAttribute("selected") && !isSelected()) { toggleSelection(); } } public void addCell(String text, char align) { // String only content is optimized by not using Label widget final Element td = DOM.createTD(); final Element container = DOM.createDiv(); DOM.setElementProperty(container, "className", CLASSNAME + "-cell-content"); DOM.setInnerHTML(container, text); if (align != ALIGN_LEFT) { switch (align) { case ALIGN_CENTER: DOM.setStyleAttribute(container, "textAlign", "center"); break; case ALIGN_RIGHT: default: DOM.setStyleAttribute(container, "textAlign", "right"); break; } } DOM.appendChild(td, container); DOM.appendChild(getElement(), td); } public void addCell(Widget w, char align) { final Element td = DOM.createTD(); final Element container = DOM.createDiv(); DOM.setElementProperty(container, "className", CLASSNAME + "-cell-content"); // TODO make widget cells respect align. text-align:center for // IE, margin: auto for others DOM.appendChild(td, container); DOM.appendChild(getElement(), td); DOM.appendChild(container, w.getElement()); adopt(w); childWidgets.add(w); } public Iterator iterator() { return childWidgets.iterator(); } public boolean remove(Widget w) { // TODO Auto-generated method stub return false; } /* * React on click that occur on content cells only */ public void onBrowserEvent(Event event) { switch (DOM.eventGetType(event)) { case Event.ONCLICK: final Element tdOrTr = DOM.getParent(DOM .eventGetTarget(event)); if (DOM.compare(getElement(), tdOrTr) || DOM.compare(getElement(), DOM.getParent(tdOrTr))) { if (selectMode > Table.SELECT_MODE_NONE) { toggleSelection(); client.updateVariable(paintableId, "selected", selectedRowKeys.toArray(), immediate); } } break; default: break; } super.onBrowserEvent(event); } public void showContextMenu(Event event) { ApplicationConnection.getConsole().log("Context menu"); if (enabled && actionKeys != null) { int left = DOM.eventGetClientX(event); int top = DOM.eventGetClientY(event); top += Window.getScrollTop(); left += Window.getScrollLeft(); client.getContextMenu().showAt(this, left, top); } } public boolean isSelected() { return selected; } private void toggleSelection() { selected = !selected; if (selected) { if (selectMode == Table.SELECT_MODE_SINGLE) { deselectAll(); } selectedRowKeys.add(String.valueOf(rowKey)); addStyleName("i-selected"); } else { selectedRowKeys.remove(String.valueOf(rowKey)); removeStyleName("i-selected"); } } /* * (non-Javadoc) * * @see com.itmill.toolkit.terminal.gwt.client.ui.IActionOwner#getActions() */ public Action[] getActions() { if (actionKeys == null) { return new Action[] {}; } final Action[] actions = new Action[actionKeys.length]; for (int i = 0; i < actions.length; i++) { final String actionKey = actionKeys[i]; final TreeAction a = new TreeAction(this, String .valueOf(rowKey), actionKey); a.setCaption(getActionCaption(actionKey)); a.setIconUrl(getActionIcon(actionKey)); actions[i] = a; } return actions; } public ApplicationConnection getClient() { return client; } public String getPaintableId() { return paintableId; } } } public void deselectAll() { final Object[] keys = selectedRowKeys.toArray(); for (int i = 0; i < keys.length; i++) { final IScrollTableRow row = getRenderedRowByKey((String) keys[i]); if (row != null && row.isSelected()) { row.toggleSelection(); } } // still ensure all selects are removed from (not necessary rendered) selectedRowKeys.clear(); } public void add(Widget w) { throw new UnsupportedOperationException( "ITable can contain only rows created by itself."); } public void clear() { panel.clear(); } public Iterator iterator() { return panel.iterator(); } public boolean remove(Widget w) { return panel.remove(w); } public void setHeight(String height) { // workaround very common 100% height problem - extract borders if (height.equals("100%")) { final int borders = getBorderSpace(); final Element elem = getElement(); final Element parentElem = DOM.getParent(elem); // put table away from flow for a moment DOM.setStyleAttribute(getElement(), "position", "absolute"); // get containers natural space for table final int availPixels = DOM.getElementPropertyInt(parentElem, "offsetHeight"); // put table back to flow DOM.setStyleAttribute(getElement(), "position", "static"); // set 100% height with borders int pixelSize = (availPixels - borders); if (pixelSize < 0) { pixelSize = 0; } super.setHeight(pixelSize + "px"); } else { // normally height don't include borders super.setHeight(height); } } private int getBorderSpace() { final Element el = getElement(); return DOM.getElementPropertyInt(el, "offsetHeight") - DOM.getElementPropertyInt(el, "clientHeight"); } }