From b22d6f609961e376d46a2a69d8c760b69c8e7485 Mon Sep 17 00:00:00 2001 From: Matti Tahvonen Date: Wed, 14 Nov 2007 08:32:55 +0000 Subject: [PATCH] IScrollTable now respects hiding column headers svn changeset:2802/svn branch:trunk --- .../terminal/gwt/client/ui/IScrollTable.java | 3837 +++++++++-------- 1 file changed, 1950 insertions(+), 1887 deletions(-) diff --git a/src/com/itmill/toolkit/terminal/gwt/client/ui/IScrollTable.java b/src/com/itmill/toolkit/terminal/gwt/client/ui/IScrollTable.java index 841823ad1c..dcb6c6222b 100644 --- a/src/com/itmill/toolkit/terminal/gwt/client/ui/IScrollTable.java +++ b/src/com/itmill/toolkit/terminal/gwt/client/ui/IScrollTable.java @@ -52,1892 +52,1955 @@ import com.itmill.toolkit.terminal.gwt.client.ui.IScrollTable.IScrollTableBody.I * 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 rowHeaders = false; - - private String[] columnOrder; - - private ApplicationConnection client; - private String paintableId; - - private boolean immediate; - - private int selectMode = Table.SELECT_MODE_NONE; - - private HashSet selectedRowKeys = new HashSet(); - - private boolean initializedAndAttached = false; - - private TableHead tHead = new TableHead(); - - private ScrollPanel bodyContainer = new ScrollPanel(); - - private int totalRows; - - private Set collapsedColumns; - - private 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 HashMap actionMap = new HashMap(); - private String[] visibleColOrder; - private boolean initialContentReceived = false; - private Element scrollPositionElement; - private FlowPanel panel; - private boolean enabled; - - 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; - - this.enabled = !uidl.hasAttribute("disabled"); - - this.client = client; - this.paintableId = uidl.getStringAttribute("id"); - this.immediate = uidl.getBooleanAttribute("immediate"); - int newTotalRows = uidl.getIntAttribute("totalrows"); - if (newTotalRows != totalRows) { - this.totalRows = newTotalRows; - if (initializedAndAttached) - tBody.setContainerHeight(); - } - - this.pageLength = uidl.getIntAttribute("pagelength"); - if (pageLength == 0) - pageLength = totalRows; - this.firstvisible = uidl.hasVariable("firstvisible") ? uidl - .getIntVariable("firstvisible") : 0; - if (uidl.hasAttribute("rowheaders")) - rowHeaders = true; - if (uidl.hasAttribute("width")) { - width = uidl.getStringAttribute("width"); - } - if (uidl.hasAttribute("height")) - height = uidl.getStringAttribute("height"); - - if (uidl.hasVariable("sortascending")) { - this.sortAscending = uidl.getBooleanVariable("sortascending"); - this.sortColumn = uidl.getStringVariable("sortcolumn"); - } - - if (uidl.hasVariable("selected")) { - Set selectedKeys = uidl.getStringArrayVariableAsSet("selected"); - selectedRowKeys.clear(); - for (Iterator it = selectedKeys.iterator(); it.hasNext();) - selectedRowKeys.add((String) 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")) { - this.columnReordering = true; - this.columnOrder = uidl.getStringArrayVariable("columnorder"); - } - - if (uidl.hasVariable("collapsedcolumns")) { - tHead.setColumnCollapsingAllowed(true); - this.collapsedColumns = uidl - .getStringArrayVariableAsSet("collapsedcolumns"); - } else { - tHead.setColumnCollapsingAllowed(false); - } - - UIDL rowData = null; - for (Iterator it = uidl.getChildIterator(); it.hasNext();) { - 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) { - Iterator it = uidl.getChildIterator(); - while (it.hasNext()) { - UIDL col = (UIDL) it.next(); - tHead.updateCellFromUIDL(col); - } - } - - private void updateActionMap(UIDL c) { - Iterator it = c.getChildIterator(); - while (it.hasNext()) { - UIDL action = (UIDL) it.next(); - String key = action.getStringAttribute("key"); - String caption = action.getStringAttribute("caption"); - actionMap.put(key + "_c", caption); - if (action.hasAttribute("icon")) { - // TODO need some uri handling ?? - actionMap.put(key + "_i", 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 (rowHeaders) { - tHead.enableColumn("0", colIndex); - visibleCols++; - visibleColOrder = new String[visibleCols]; - visibleColOrder[colIndex] = "0"; - colIndex++; - } else { - visibleColOrder = new String[visibleCols]; - } - - for (int i = 0; i < strings.length; i++) { - String cid = strings[i]; - visibleColOrder[colIndex] = cid; - tHead.enableColumn(cid, colIndex); - colIndex++; - - } - - } - - /** - * @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); - - int optimalFirstRow = (int) (firstRowInViewPort - pageLength - * CACHE_RATE); - while (tBody.getFirstRendered() < optimalFirstRow) { - // client.console.log("removing row from start"); - tBody.unlinkRow(true); - } - int optimalLastRow = (int) (firstRowInViewPort + pageLength + pageLength - * CACHE_RATE); - while (tBody.getLastRendered() > optimalLastRow) { - // client.console.log("removing row from the end"); - tBody.unlinkRow(false); - } - - } - - /** - * 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) { - 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) { - 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) { - - 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 - */ - String oldKeyOnNewIndex = visibleColOrder[newIndex]; - if (rowHeaders) - 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 - 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 = rowHeaders ? 1 : 0; - for (int j = 0; j < newOrder.length; j++) { - 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) { - 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; - - int[] widths = new int[tHead.visibleCells.size()]; - - // first loop: collect natural widths - while (headCells.hasNext()) { - HeaderCell hCell = (HeaderCell) headCells.next(); - int w; - if (hCell.getWidth() > 0) { - // server has defined column width explicitly - w = hCell.getWidth(); - totalExplicitColumnsWidths += w; - } else { - int hw = DOM.getElementPropertyInt(hCell.getElement(), - "offsetWidth"); - 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"); - this.setWidth(w + "px"); - } else { - if (width.indexOf("px") > 0) { - bodyContainer.setWidth(width); - tHead.setWidth(width); - this.setWidth(width); - } else if (width.indexOf("%") > 0) { - if (!width.equals("100%")) - this.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 - int extraSpace = availW - total; - 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]; - 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()) { - HeaderCell hCell = (HeaderCell) headCells.next(); - if (hCell.getWidth() == -1) { - 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) { - // TODO cancel scroll (scrorll back or something) - 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; - int lastRendered = tBody.getLastRendered(); - int firstRendered = tBody.getFirstRendered(); - - if (postLimit <= lastRendered && preLimit >= firstRendered) { - client.updateVariable(this.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; - } - this.setReqFirstRow(first); - this.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 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; - - private HeaderCell() { - }; - - public void setSortable(boolean b) { - sortable = b; - } - - public HeaderCell(String colId, String headerText) { - this.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) { - this.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); - // TODO isolate non-standard css attribute (filter) - // TODO move styles to css file - 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()); - this.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; - } - int x = DOM.eventGetClientX(event) - + DOM.getElementPropertyInt(tHead.hTableWrapper, - "scrollLeft"); - int slotX = headerX; - closestSlot = colIndex; - int closestDistance = -1; - int start = 0; - if (rowHeaders) { - start++; - } - int visibleCellCount = tHead.getVisibleCellCount(); - for (int i = start; i <= visibleCellCount; i++) { - if (i > 0) { - String colKey = getColKeyByIndex(i - 1); - slotX += getColWidth(colKey); - } - 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) { - 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 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) { - 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")) { - String width = col.getStringAttribute("width"); - c.setWidth(Integer.parseInt(width)); - } - // TODO icon - } - - public void enableColumn(String cid, int index) { - 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) { - HeaderCell hCell = getHeaderCell(oldIndex); - 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) { - 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)) { - int left = DOM.getAbsoluteLeft(columnSelector); - 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() { - 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 (IScrollTable.this.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 (Iterator it = collapsedColumns.iterator(); it.hasNext();) - cols[i++] = it.next(); - } - Action[] actions = new Action[cols.length]; - - for (int i = 0; i < cols.length; i++) { - String cid = (String) cols[i]; - HeaderCell c = getHeaderCell(cid); - 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() { - Iterator it = visibleCells.iterator(); - 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 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) { - this.firstRendered = firstIndex; - this.lastRendered = firstIndex + rows - 1; - Iterator it = rowData.getChildIterator(); - aligns = tHead.getColumnAlignments(); - while (it.hasNext()) { - 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(); - Iterator it = rowData.getChildIterator(); - if (firstIndex == lastRendered + 1) { - while (it.hasNext()) { - IScrollTableRow row = createRow((UIDL) it.next()); - addRow(row); - lastRendered++; - } - fixSpacers(); - } else if (firstIndex + rows == firstRendered) { - 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 - // create one row before truncating row - IScrollTableRow row = createRow((UIDL) it.next()); - while (lastRendered + 1 > firstRendered) - unlinkRow(false); - firstRendered = firstIndex; - lastRendered = firstIndex - 1; - fixSpacers(); - addRow(row); - lastRendered++; - 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) { - IScrollTableRow row = new IScrollTableRow(uidl, aligns); - int cells = DOM.getChildCount(row.getElement()); - for (int i = 0; i < cells; i++) { - Element cell = DOM.getChild(row.getElement(), i); - 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--; - } - IScrollTableRow toBeRemoved = (IScrollTableRow) renderedRows - .get(index); - client.unregisterChildPaintables(toBeRemoved); - DOM.removeChild(tBody, toBeRemoved.getElement()); - this.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() { - DOM.setStyleAttribute(preSpacer, "height", getRowHeight() - * firstRendered + "px"); - DOM.setStyleAttribute(postSpacer, "height", getRowHeight() - * (totalRows - 1 - lastRendered) + "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) { - Element e = DOM.getChild(DOM.getChild(tBody, 0), i); - return DOM.getElementPropertyInt(e, "offsetWidth"); - } else { - return 0; - } - } - - public void setColWidth(int colIndex, int w) { - int rows = DOM.getChildCount(tBody); - for (int i = 0; i < rows; i++) { - 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 - Iterator rows = iterator(); - while (rows.hasNext()) { - IScrollTableRow row = (IScrollTableRow) rows.next(); - - 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 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 (rowHeaders) { - addCell(uidl.getStringAttribute("caption"), aligns[col++]); - } - - if (uidl.hasAttribute("al")) - actionKeys = uidl.getStringArrayAttribute("al"); - - Iterator cells = uidl.getChildIterator(); - while (cells.hasNext()) { - Object cell = cells.next(); - if (cell instanceof String) { - addCell(cell.toString(), aligns[col++]); - } else { - 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 - Element td = DOM.createTD(); - 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) { - Element td = DOM.createTD(); - 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) { - String s = DOM.getElementProperty(DOM.eventGetTarget(event), - "className"); - switch (DOM.eventGetType(event)) { - case Event.ONCLICK: - if ((CLASSNAME + "-cell-content").equals(s)) { - ApplicationConnection.getConsole().log("Row click"); - 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 (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) - IScrollTable.this.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[] {}; - Action[] actions = new Action[actionKeys.length]; - for (int i = 0; i < actions.length; i++) { - String actionKey = actionKeys[i]; - 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() { - Object[] keys = selectedRowKeys.toArray(); - for (int i = 0; i < keys.length; i++) { - 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(); - Element elem = getElement(); - Element parentElem = DOM.getParent(elem); - - // put table away from flow for a moment - DOM.setStyleAttribute(getElement(), "position", "absolute"); - // get containers natural space for table - int availPixels = DOM.getElementPropertyInt(parentElem, - "clientHeight"); - // 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() { - Element el = getElement(); - return DOM.getElementPropertyInt(el, "offsetHeight") - - DOM.getElementPropertyInt(el, "clientHeight"); - } + 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 HashSet selectedRowKeys = new HashSet(); + + private boolean initializedAndAttached = false; + + private TableHead tHead = new TableHead(); + + private ScrollPanel bodyContainer = new ScrollPanel(); + + private int totalRows; + + private Set collapsedColumns; + + private 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 HashMap actionMap = new HashMap(); + private String[] visibleColOrder; + private boolean initialContentReceived = false; + private Element scrollPositionElement; + private 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"); + 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")) { + Set selectedKeys = uidl.getStringArrayVariableAsSet("selected"); + selectedRowKeys.clear(); + for (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 (Iterator it = uidl.getChildIterator(); it.hasNext();) { + 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) { + Iterator it = uidl.getChildIterator(); + while (it.hasNext()) { + UIDL col = (UIDL) it.next(); + tHead.updateCellFromUIDL(col); + } + } + + private void updateActionMap(UIDL c) { + Iterator it = c.getChildIterator(); + while (it.hasNext()) { + UIDL action = (UIDL) it.next(); + String key = action.getStringAttribute("key"); + String caption = action.getStringAttribute("caption"); + actionMap.put(key + "_c", caption); + if (action.hasAttribute("icon")) { + // TODO need some uri handling ?? + actionMap.put(key + "_i", 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++) { + 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); + + int optimalFirstRow = (int) (firstRowInViewPort - pageLength + * CACHE_RATE); + while (tBody.getFirstRendered() < optimalFirstRow) { + // client.console.log("removing row from start"); + tBody.unlinkRow(true); + } + int optimalLastRow = (int) (firstRowInViewPort + pageLength + pageLength + * CACHE_RATE); + while (tBody.getLastRendered() > optimalLastRow) { + // client.console.log("removing row from the end"); + tBody.unlinkRow(false); + } + + } + + /** + * 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) { + 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) { + 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) { + + 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 + */ + 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 + 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++) { + 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) { + 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; + + int[] widths = new int[tHead.visibleCells.size()]; + + // first loop: collect natural widths + while (headCells.hasNext()) { + HeaderCell hCell = (HeaderCell) headCells.next(); + int w; + if (hCell.getWidth() > 0) { + // server has defined column width explicitly + w = hCell.getWidth(); + totalExplicitColumnsWidths += w; + } else { + int hw = DOM.getElementPropertyInt(hCell.getElement(), + "offsetWidth"); + 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 + int extraSpace = availW - total; + 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]; + 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()) { + HeaderCell hCell = (HeaderCell) headCells.next(); + if (hCell.getWidth() == -1) { + 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) { + // TODO cancel scroll (scrorll back or something) + 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; + } + int lastRendered = tBody.getLastRendered(); + 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 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; + + private HeaderCell() { + }; + + 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); + // TODO isolate non-standard css attribute (filter) + // TODO move styles to css file + 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; + } + int x = DOM.eventGetClientX(event) + + DOM.getElementPropertyInt(tHead.hTableWrapper, + "scrollLeft"); + int slotX = headerX; + closestSlot = colIndex; + int closestDistance = -1; + int start = 0; + if (showRowHeaders) { + start++; + } + int visibleCellCount = tHead.getVisibleCellCount(); + for (int i = start; i <= visibleCellCount; i++) { + if (i > 0) { + String colKey = getColKeyByIndex(i - 1); + slotX += getColWidth(colKey); + } + 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) { + 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 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) { + 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")) { + String width = col.getStringAttribute("width"); + c.setWidth(Integer.parseInt(width)); + } + // TODO icon + } + + public void enableColumn(String cid, int index) { + 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) { + HeaderCell hCell = getHeaderCell(oldIndex); + 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) { + 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)) { + int left = DOM.getAbsoluteLeft(columnSelector); + 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() { + 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 (Iterator it = collapsedColumns.iterator(); it.hasNext();) { + cols[i++] = it.next(); + } + } + Action[] actions = new Action[cols.length]; + + for (int i = 0; i < cols.length; i++) { + String cid = (String) cols[i]; + HeaderCell c = getHeaderCell(cid); + 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() { + Iterator it = visibleCells.iterator(); + 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 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; + Iterator it = rowData.getChildIterator(); + aligns = tHead.getColumnAlignments(); + while (it.hasNext()) { + 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(); + Iterator it = rowData.getChildIterator(); + if (firstIndex == lastRendered + 1) { + while (it.hasNext()) { + IScrollTableRow row = createRow((UIDL) it.next()); + addRow(row); + lastRendered++; + } + fixSpacers(); + } else if (firstIndex + rows == firstRendered) { + 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 + // create one row before truncating row + IScrollTableRow row = createRow((UIDL) it.next()); + while (lastRendered + 1 > firstRendered) { + unlinkRow(false); + } + firstRendered = firstIndex; + lastRendered = firstIndex - 1; + fixSpacers(); + addRow(row); + lastRendered++; + 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) { + IScrollTableRow row = new IScrollTableRow(uidl, aligns); + int cells = DOM.getChildCount(row.getElement()); + for (int i = 0; i < cells; i++) { + Element cell = DOM.getChild(row.getElement(), i); + 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--; + } + 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() { + DOM.setStyleAttribute(preSpacer, "height", getRowHeight() + * firstRendered + "px"); + DOM.setStyleAttribute(postSpacer, "height", getRowHeight() + * (totalRows - 1 - lastRendered) + "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) { + Element e = DOM.getChild(DOM.getChild(tBody, 0), i); + return DOM.getElementPropertyInt(e, "offsetWidth"); + } else { + return 0; + } + } + + public void setColWidth(int colIndex, int w) { + int rows = DOM.getChildCount(tBody); + for (int i = 0; i < rows; i++) { + 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 + Iterator rows = iterator(); + while (rows.hasNext()) { + IScrollTableRow row = (IScrollTableRow) rows.next(); + + 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 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"); + } + + Iterator cells = uidl.getChildIterator(); + while (cells.hasNext()) { + Object cell = cells.next(); + if (cell instanceof String) { + addCell(cell.toString(), aligns[col++]); + } else { + 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 + Element td = DOM.createTD(); + 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) { + Element td = DOM.createTD(); + 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) { + String s = DOM.getElementProperty(DOM.eventGetTarget(event), + "className"); + switch (DOM.eventGetType(event)) { + case Event.ONCLICK: + if ((CLASSNAME + "-cell-content").equals(s)) { + ApplicationConnection.getConsole().log("Row click"); + 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 (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[] {}; + } + Action[] actions = new Action[actionKeys.length]; + for (int i = 0; i < actions.length; i++) { + String actionKey = actionKeys[i]; + 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() { + Object[] keys = selectedRowKeys.toArray(); + for (int i = 0; i < keys.length; i++) { + 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(); + Element elem = getElement(); + Element parentElem = DOM.getParent(elem); + + // put table away from flow for a moment + DOM.setStyleAttribute(getElement(), "position", "absolute"); + // get containers natural space for table + int availPixels = DOM.getElementPropertyInt(parentElem, + "clientHeight"); + // 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() { + Element el = getElement(); + return DOM.getElementPropertyInt(el, "offsetHeight") + - DOM.getElementPropertyInt(el, "clientHeight"); + } } -- 2.39.5