* 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, "<span>" + firstRowInViewPort
- + " – " + last + "..." + "</span>");
- 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("<span class=\"i-off\">");
- buf.append(super.getHTML());
- if (collapsed)
- buf.append("</span>");
- 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, "<span>" + firstRowInViewPort
+ + " – " + last + "..." + "</span>");
+ 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("<span class=\"i-off\">");
+ }
+ buf.append(super.getHTML());
+ if (collapsed) {
+ buf.append("</span>");
+ }
+ 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");
+ }
}