]> source.dussan.org Git - vaadin-framework.git/commitdiff
Merge commit '99786'
authorJohannes Dahlström <johannesd@vaadin.com>
Mon, 11 Jun 2012 11:18:27 +0000 (14:18 +0300)
committerJohannes Dahlström <johannesd@vaadin.com>
Mon, 11 Jun 2012 11:18:27 +0000 (14:18 +0300)
Conflicts:
src/com/vaadin/ui/TabSheet.java

1  2 
src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java
src/com/vaadin/ui/TabSheet.java
src/com/vaadin/ui/Table.java

index f7cd9d133e15844ec6e7c24426650aa8c387edc7,0000000000000000000000000000000000000000..9a8e0e9ce10f3b2f61695dca7b751ed0c339e6d8
mode 100644,000000..100644
--- /dev/null
@@@ -1,799 -1,0 +1,815 @@@
-             RowCollapseAnimation anim = new RowCollapseAnimation(rowsToDelete) {
-                 @Override
-                 protected void onComplete() {
-                     super.onComplete();
-                     // Actually unlink the rows and update the cache after the
-                     // animation is done.
-                     unlinkAndReindexRows(firstIndex, rows);
-                     discardRowsOutsideCacheWindow();
-                     ensureCacheFilled();
-                 }
-             };
-             anim.run(150);
 +/*
 +@VaadinApache2LicenseForJavaFiles@
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.treetable;
 +
 +import java.util.ArrayList;
 +import java.util.Iterator;
 +import java.util.LinkedList;
 +import java.util.List;
 +
 +import com.google.gwt.animation.client.Animation;
 +import com.google.gwt.core.client.Scheduler;
 +import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 +import com.google.gwt.dom.client.Document;
 +import com.google.gwt.dom.client.ImageElement;
 +import com.google.gwt.dom.client.SpanElement;
 +import com.google.gwt.dom.client.Style.Display;
 +import com.google.gwt.dom.client.Style.Unit;
 +import com.google.gwt.dom.client.Style.Visibility;
 +import com.google.gwt.dom.client.TableCellElement;
 +import com.google.gwt.event.dom.client.KeyCodes;
 +import com.google.gwt.user.client.DOM;
 +import com.google.gwt.user.client.Element;
 +import com.google.gwt.user.client.Event;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.terminal.gwt.client.ComputedStyle;
 +import com.vaadin.terminal.gwt.client.UIDL;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable;
 +import com.vaadin.terminal.gwt.client.ui.treetable.VTreeTable.VTreeTableScrollBody.VTreeTableRow;
 +
 +public class VTreeTable extends VScrollTable {
 +
 +    static class PendingNavigationEvent {
 +        final int keycode;
 +        final boolean ctrl;
 +        final boolean shift;
 +
 +        public PendingNavigationEvent(int keycode, boolean ctrl, boolean shift) {
 +            this.keycode = keycode;
 +            this.ctrl = ctrl;
 +            this.shift = shift;
 +        }
 +
 +        @Override
 +        public String toString() {
 +            String string = "Keyboard event: " + keycode;
 +            if (ctrl) {
 +                string += " + ctrl";
 +            }
 +            if (shift) {
 +                string += " + shift";
 +            }
 +            return string;
 +        }
 +    }
 +
 +    boolean collapseRequest;
 +    private boolean selectionPending;
 +    int colIndexOfHierarchy;
 +    String collapsedRowKey;
 +    VTreeTableScrollBody scrollBody;
 +    boolean animationsEnabled;
 +    LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTable.PendingNavigationEvent>();
 +    boolean focusParentResponsePending;
 +
 +    @Override
 +    protected VScrollTableBody createScrollBody() {
 +        scrollBody = new VTreeTableScrollBody();
 +        return scrollBody;
 +    }
 +
 +    /*
 +     * Overridden to allow animation of expands and collapses of nodes.
 +     */
 +    @Override
 +    protected void addAndRemoveRows(UIDL partialRowAdditions) {
 +        if (partialRowAdditions == null) {
 +            return;
 +        }
 +
 +        if (animationsEnabled) {
 +            if (partialRowAdditions.hasAttribute("hide")) {
 +                scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished(
 +                        partialRowAdditions.getIntAttribute("firstprowix"),
 +                        partialRowAdditions.getIntAttribute("numprows"));
 +            } else {
 +                scrollBody.insertRowsAnimated(partialRowAdditions,
 +                        partialRowAdditions.getIntAttribute("firstprowix"),
 +                        partialRowAdditions.getIntAttribute("numprows"));
 +                discardRowsOutsideCacheWindow();
 +            }
 +        } else {
 +            super.addAndRemoveRows(partialRowAdditions);
 +        }
 +    }
 +
 +    class VTreeTableScrollBody extends VScrollTable.VScrollTableBody {
 +        private int identWidth = -1;
 +
 +        VTreeTableScrollBody() {
 +            super();
 +        }
 +
 +        @Override
 +        protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
 +            if (uidl.hasAttribute("gen_html")) {
 +                // This is a generated row.
 +                return new VTreeTableGeneratedRow(uidl, aligns2);
 +            }
 +            return new VTreeTableRow(uidl, aligns2);
 +        }
 +
 +        class VTreeTableRow extends
 +                VScrollTable.VScrollTableBody.VScrollTableRow {
 +
 +            private boolean isTreeCellAdded = false;
 +            private SpanElement treeSpacer;
 +            private boolean open;
 +            private int depth;
 +            private boolean canHaveChildren;
 +            protected Widget widgetInHierarchyColumn;
 +
 +            public VTreeTableRow(UIDL uidl, char[] aligns2) {
 +                super(uidl, aligns2);
 +            }
 +
 +            @Override
 +            public void addCell(UIDL rowUidl, String text, char align,
 +                    String style, boolean textIsHTML, boolean isSorted,
 +                    String description) {
 +                super.addCell(rowUidl, text, align, style, textIsHTML,
 +                        isSorted, description);
 +
 +                addTreeSpacer(rowUidl);
 +            }
 +
 +            protected boolean addTreeSpacer(UIDL rowUidl) {
 +                if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
 +                    Element container = (Element) getElement().getLastChild()
 +                            .getFirstChild();
 +
 +                    if (rowUidl.hasAttribute("icon")) {
 +                        // icons are in first content cell in TreeTable
 +                        ImageElement icon = Document.get().createImageElement();
 +                        icon.setClassName("v-icon");
 +                        icon.setAlt("icon");
 +                        icon.setSrc(client.translateVaadinUri(rowUidl
 +                                .getStringAttribute("icon")));
 +                        container.insertFirst(icon);
 +                    }
 +
 +                    String classname = "v-treetable-treespacer";
 +                    if (rowUidl.getBooleanAttribute("ca")) {
 +                        canHaveChildren = true;
 +                        open = rowUidl.getBooleanAttribute("open");
 +                        classname += open ? " v-treetable-node-open"
 +                                : " v-treetable-node-closed";
 +                    }
 +
 +                    treeSpacer = Document.get().createSpanElement();
 +
 +                    treeSpacer.setClassName(classname);
 +                    container.insertFirst(treeSpacer);
 +                    depth = rowUidl.hasAttribute("depth") ? rowUidl
 +                            .getIntAttribute("depth") : 0;
 +                    setIdent();
 +                    isTreeCellAdded = true;
 +                    return true;
 +                }
 +                return false;
 +            }
 +
 +            private boolean cellShowsTreeHierarchy(int curColIndex) {
 +                if (isTreeCellAdded) {
 +                    return false;
 +                }
 +                return curColIndex == colIndexOfHierarchy
 +                        + (showRowHeaders ? 1 : 0);
 +            }
 +
 +            @Override
 +            public void onBrowserEvent(Event event) {
 +                if (event.getEventTarget().cast() == treeSpacer
 +                        && treeSpacer.getClassName().contains("node")) {
 +                    if (event.getTypeInt() == Event.ONMOUSEUP) {
 +                        sendToggleCollapsedUpdate(getKey());
 +                    }
 +                    return;
 +                }
 +                super.onBrowserEvent(event);
 +            }
 +
 +            @Override
 +            public void addCell(UIDL rowUidl, Widget w, char align,
 +                    String style, boolean isSorted) {
 +                super.addCell(rowUidl, w, align, style, isSorted);
 +                if (addTreeSpacer(rowUidl)) {
 +                    widgetInHierarchyColumn = w;
 +                }
 +
 +            }
 +
 +            private void setIdent() {
 +                if (getIdentWidth() > 0 && depth != 0) {
 +                    treeSpacer.getStyle().setWidth(
 +                            (depth + 1) * getIdentWidth(), Unit.PX);
 +                }
 +            }
 +
 +            @Override
 +            protected void onAttach() {
 +                super.onAttach();
 +                if (getIdentWidth() < 0) {
 +                    detectIdent(this);
 +                }
 +            }
 +
 +            private int getHierarchyAndIconWidth() {
 +                int consumedSpace = treeSpacer.getOffsetWidth();
 +                if (treeSpacer.getParentElement().getChildCount() > 2) {
 +                    // icon next to tree spacer
 +                    consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer
 +                            .getNextSibling()).getOffsetWidth();
 +                }
 +                return consumedSpace;
 +            }
 +
 +        }
 +
 +        protected class VTreeTableGeneratedRow extends VTreeTableRow {
 +            private boolean spanColumns;
 +            private boolean htmlContentAllowed;
 +
 +            public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) {
 +                super(uidl, aligns);
 +                addStyleName("v-table-generated-row");
 +            }
 +
 +            public boolean isSpanColumns() {
 +                return spanColumns;
 +            }
 +
 +            @Override
 +            protected void initCellWidths() {
 +                if (spanColumns) {
 +                    setSpannedColumnWidthAfterDOMFullyInited();
 +                } else {
 +                    super.initCellWidths();
 +                }
 +            }
 +
 +            private void setSpannedColumnWidthAfterDOMFullyInited() {
 +                // Defer setting width on spanned columns to make sure that
 +                // they are added to the DOM before trying to calculate
 +                // widths.
 +                Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 +
 +                    public void execute() {
 +                        if (showRowHeaders) {
 +                            setCellWidth(0, tHead.getHeaderCell(0).getWidth());
 +                            calcAndSetSpanWidthOnCell(1);
 +                        } else {
 +                            calcAndSetSpanWidthOnCell(0);
 +                        }
 +                    }
 +                });
 +            }
 +
 +            @Override
 +            protected boolean isRenderHtmlInCells() {
 +                return htmlContentAllowed;
 +            }
 +
 +            @Override
 +            protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
 +                    int visibleColumnIndex) {
 +                htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
 +                spanColumns = uidl.getBooleanAttribute("gen_span");
 +
 +                final Iterator<?> cells = uidl.getChildIterator();
 +                if (spanColumns) {
 +                    int colCount = uidl.getChildCount();
 +                    if (cells.hasNext()) {
 +                        final Object cell = cells.next();
 +                        if (cell instanceof String) {
 +                            addSpannedCell(uidl, cell.toString(), aligns[0],
 +                                    "", htmlContentAllowed, false, null,
 +                                    colCount);
 +                        } else {
 +                            addSpannedCell(uidl, (Widget) cell, aligns[0], "",
 +                                    false, colCount);
 +                        }
 +                    }
 +                } else {
 +                    super.addCellsFromUIDL(uidl, aligns, col,
 +                            visibleColumnIndex);
 +                }
 +            }
 +
 +            private void addSpannedCell(UIDL rowUidl, Widget w, char align,
 +                    String style, boolean sorted, int colCount) {
 +                TableCellElement td = DOM.createTD().cast();
 +                td.setColSpan(colCount);
 +                initCellWithWidget(w, align, style, sorted, td);
 +                td.getStyle().setHeight(getRowHeight(), Unit.PX);
 +                if (addTreeSpacer(rowUidl)) {
 +                    widgetInHierarchyColumn = w;
 +                }
 +            }
 +
 +            private void addSpannedCell(UIDL rowUidl, String text, char align,
 +                    String style, boolean textIsHTML, boolean sorted,
 +                    String description, int colCount) {
 +                // String only content is optimized by not using Label widget
 +                final TableCellElement td = DOM.createTD().cast();
 +                td.setColSpan(colCount);
 +                initCellWithText(text, align, style, textIsHTML, sorted,
 +                        description, td);
 +                td.getStyle().setHeight(getRowHeight(), Unit.PX);
 +                addTreeSpacer(rowUidl);
 +            }
 +
 +            @Override
 +            protected void setCellWidth(int cellIx, int width) {
 +                if (isSpanColumns()) {
 +                    if (showRowHeaders) {
 +                        if (cellIx == 0) {
 +                            super.setCellWidth(0, width);
 +                        } else {
 +                            // We need to recalculate the spanning TDs width for
 +                            // every cellIx in order to support column resizing.
 +                            calcAndSetSpanWidthOnCell(1);
 +                        }
 +                    } else {
 +                        // Same as above.
 +                        calcAndSetSpanWidthOnCell(0);
 +                    }
 +                } else {
 +                    super.setCellWidth(cellIx, width);
 +                }
 +            }
 +
 +            private void calcAndSetSpanWidthOnCell(final int cellIx) {
 +                int spanWidth = 0;
 +                for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
 +                        .getVisibleCellCount(); ix++) {
 +                    spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
 +                }
 +                Util.setWidthExcludingPaddingAndBorder((Element) getElement()
 +                        .getChild(cellIx), spanWidth, 13, false);
 +            }
 +        }
 +
 +        private int getIdentWidth() {
 +            return identWidth;
 +        }
 +
 +        private void detectIdent(VTreeTableRow vTreeTableRow) {
 +            identWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
 +            if (identWidth == 0) {
 +                identWidth = -1;
 +                return;
 +            }
 +            Iterator<Widget> iterator = iterator();
 +            while (iterator.hasNext()) {
 +                VTreeTableRow next = (VTreeTableRow) iterator.next();
 +                next.setIdent();
 +            }
 +        }
 +
 +        protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished(
 +                final int firstIndex, final int rows) {
 +            List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>();
 +            for (int ix = firstIndex; ix < firstIndex + rows; ix++) {
 +                VScrollTableRow row = getRowByRowIndex(ix);
 +                if (row != null) {
 +                    rowsToDelete.add(row);
 +                }
 +            }
-             RowExpandAnimation anim = new RowExpandAnimation(insertedRows);
-             anim.run(150);
++            if (!rowsToDelete.isEmpty()) {
++                // #8810 Only animate if there's something to animate
++                RowCollapseAnimation anim = new RowCollapseAnimation(
++                        rowsToDelete) {
++                    @Override
++                    protected void onComplete() {
++                        super.onComplete();
++                        // Actually unlink the rows and update the cache after
++                        // the
++                        // animation is done.
++                        unlinkAndReindexRows(firstIndex, rows);
++                        discardRowsOutsideCacheWindow();
++                        ensureCacheFilled();
++                    }
++                };
++                anim.run(150);
++            }
 +        }
 +
 +        protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData,
 +                int firstIndex, int rows) {
 +            List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData,
 +                    firstIndex, rows);
++            if (!insertedRows.isEmpty()) {
++                // Only animate if there's something to animate (#8810)
++                RowExpandAnimation anim = new RowExpandAnimation(insertedRows);
++                anim.run(150);
++            }
 +            return insertedRows;
 +        }
 +
 +        /**
 +         * Prepares the table for animation by copying the background colors of
 +         * all TR elements to their respective TD elements if the TD element is
 +         * transparent. This is needed, since if TDs have transparent
 +         * backgrounds, the rows sliding behind them are visible.
 +         */
 +        private class AnimationPreparator {
 +            private final int lastItemIx;
 +
 +            public AnimationPreparator(int lastItemIx) {
 +                this.lastItemIx = lastItemIx;
 +            }
 +
 +            public void prepareTableForAnimation() {
 +                int ix = lastItemIx;
 +                VScrollTableRow row = null;
 +                while ((row = getRowByRowIndex(ix)) != null) {
 +                    copyTRBackgroundsToTDs(row);
 +                    --ix;
 +                }
 +            }
 +
 +            private void copyTRBackgroundsToTDs(VScrollTableRow row) {
 +                Element tr = row.getElement();
 +                ComputedStyle cs = new ComputedStyle(tr);
 +                String backgroundAttachment = cs
 +                        .getProperty("backgroundAttachment");
 +                String backgroundClip = cs.getProperty("backgroundClip");
 +                String backgroundColor = cs.getProperty("backgroundColor");
 +                String backgroundImage = cs.getProperty("backgroundImage");
 +                String backgroundOrigin = cs.getProperty("backgroundOrigin");
 +                for (int ix = 0; ix < tr.getChildCount(); ix++) {
 +                    Element td = tr.getChild(ix).cast();
 +                    if (!elementHasBackground(td)) {
 +                        td.getStyle().setProperty("backgroundAttachment",
 +                                backgroundAttachment);
 +                        td.getStyle().setProperty("backgroundClip",
 +                                backgroundClip);
 +                        td.getStyle().setProperty("backgroundColor",
 +                                backgroundColor);
 +                        td.getStyle().setProperty("backgroundImage",
 +                                backgroundImage);
 +                        td.getStyle().setProperty("backgroundOrigin",
 +                                backgroundOrigin);
 +                    }
 +                }
 +            }
 +
 +            private boolean elementHasBackground(Element element) {
 +                ComputedStyle cs = new ComputedStyle(element);
 +                String clr = cs.getProperty("backgroundColor");
 +                String img = cs.getProperty("backgroundImage");
 +                return !("rgba(0, 0, 0, 0)".equals(clr.trim())
 +                        || "transparent".equals(clr.trim()) || img == null);
 +            }
 +
 +            public void restoreTableAfterAnimation() {
 +                int ix = lastItemIx;
 +                VScrollTableRow row = null;
 +                while ((row = getRowByRowIndex(ix)) != null) {
 +                    restoreStyleForTDsInRow(row);
 +
 +                    --ix;
 +                }
 +            }
 +
 +            private void restoreStyleForTDsInRow(VScrollTableRow row) {
 +                Element tr = row.getElement();
 +                for (int ix = 0; ix < tr.getChildCount(); ix++) {
 +                    Element td = tr.getChild(ix).cast();
 +                    td.getStyle().clearProperty("backgroundAttachment");
 +                    td.getStyle().clearProperty("backgroundClip");
 +                    td.getStyle().clearProperty("backgroundColor");
 +                    td.getStyle().clearProperty("backgroundImage");
 +                    td.getStyle().clearProperty("backgroundOrigin");
 +                }
 +            }
 +        }
 +
 +        /**
 +         * Animates row expansion using the GWT animation framework.
 +         * 
 +         * The idea is as follows:
 +         * 
 +         * 1. Insert all rows normally
 +         * 
 +         * 2. Insert a newly created DIV containing a new TABLE element below
 +         * the DIV containing the actual scroll table body.
 +         * 
 +         * 3. Clone the rows that were inserted in step 1 and attach the clones
 +         * to the new TABLE element created in step 2.
 +         * 
 +         * 4. The new DIV from step 2 is absolutely positioned so that the last
 +         * inserted row is just behind the row that was expanded.
 +         * 
 +         * 5. Hide the contents of the originally inserted rows by setting the
 +         * DIV.v-table-cell-wrapper to display:none;.
 +         * 
 +         * 6. Set the height of the originally inserted rows to 0.
 +         * 
 +         * 7. The animation loop slides the DIV from step 2 downwards, while at
 +         * the same pace growing the height of each of the inserted rows from 0
 +         * to full height. The first inserted row grows from 0 to full and after
 +         * this the second row grows from 0 to full, etc until all rows are full
 +         * height.
 +         * 
 +         * 8. Remove the DIV from step 2
 +         * 
 +         * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements.
 +         * 
 +         * 10. DONE
 +         */
 +        private class RowExpandAnimation extends Animation {
 +
 +            private final List<VScrollTableRow> rows;
 +            private Element cloneDiv;
 +            private Element cloneTable;
 +            private AnimationPreparator preparator;
 +
++            /**
++             * @param rows
++             *            List of rows to animate. Must not be empty.
++             */
 +            public RowExpandAnimation(List<VScrollTableRow> rows) {
 +                this.rows = rows;
 +                buildAndInsertAnimatingDiv();
 +                preparator = new AnimationPreparator(rows.get(0).getIndex() - 1);
 +                preparator.prepareTableForAnimation();
 +                for (VScrollTableRow row : rows) {
 +                    cloneAndAppendRow(row);
 +                    row.addStyleName("v-table-row-animating");
 +                    setCellWrapperDivsToDisplayNone(row);
 +                    row.setHeight(getInitialHeight());
 +                }
 +            }
 +
 +            protected String getInitialHeight() {
 +                return "0px";
 +            }
 +
 +            private void cloneAndAppendRow(VScrollTableRow row) {
 +                Element clonedTR = null;
 +                clonedTR = row.getElement().cloneNode(true).cast();
 +                clonedTR.getStyle().setVisibility(Visibility.VISIBLE);
 +                cloneTable.appendChild(clonedTR);
 +            }
 +
 +            protected double getBaseOffset() {
 +                return rows.get(0).getAbsoluteTop()
 +                        - rows.get(0).getParent().getAbsoluteTop()
 +                        - rows.size() * getRowHeight();
 +            }
 +
 +            private void buildAndInsertAnimatingDiv() {
 +                cloneDiv = DOM.createDiv();
 +                cloneDiv.addClassName("v-treetable-animation-clone-wrapper");
 +                cloneTable = DOM.createTable();
 +                cloneTable.addClassName("v-treetable-animation-clone");
 +                cloneDiv.appendChild(cloneTable);
 +                insertAnimatingDiv();
 +            }
 +
 +            private void insertAnimatingDiv() {
 +                Element tableBody = getElement().cast();
 +                Element tableBodyParent = tableBody.getParentElement().cast();
 +                tableBodyParent.insertAfter(cloneDiv, tableBody);
 +            }
 +
 +            @Override
 +            protected void onUpdate(double progress) {
 +                animateDiv(progress);
 +                animateRowHeights(progress);
 +            }
 +
 +            private void animateDiv(double progress) {
 +                double offset = calculateDivOffset(progress, getRowHeight());
 +
 +                cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX);
 +            }
 +
 +            private void animateRowHeights(double progress) {
 +                double rh = getRowHeight();
 +                double vlh = calculateHeightOfAllVisibleLines(progress, rh);
 +                int ix = 0;
 +
 +                while (ix < rows.size()) {
 +                    double height = vlh < rh ? vlh : rh;
 +                    rows.get(ix).setHeight(height + "px");
 +                    vlh -= height;
 +                    ix++;
 +                }
 +            }
 +
 +            protected double calculateHeightOfAllVisibleLines(double progress,
 +                    double rh) {
 +                return rows.size() * rh * progress;
 +            }
 +
 +            protected double calculateDivOffset(double progress, double rh) {
 +                return progress * rows.size() * rh;
 +            }
 +
 +            @Override
 +            protected void onComplete() {
 +                preparator.restoreTableAfterAnimation();
 +                for (VScrollTableRow row : rows) {
 +                    resetCellWrapperDivsDisplayProperty(row);
 +                    row.removeStyleName("v-table-row-animating");
 +                }
 +                Element tableBodyParent = (Element) getElement()
 +                        .getParentElement();
 +                tableBodyParent.removeChild(cloneDiv);
 +            }
 +
 +            private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) {
 +                Element tr = row.getElement();
 +                for (int ix = 0; ix < tr.getChildCount(); ix++) {
 +                    getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE);
 +                }
 +            }
 +
 +            private Element getWrapperDiv(Element tr, int tdIx) {
 +                Element td = tr.getChild(tdIx).cast();
 +                return td.getChild(0).cast();
 +            }
 +
 +            private void resetCellWrapperDivsDisplayProperty(VScrollTableRow row) {
 +                Element tr = row.getElement();
 +                for (int ix = 0; ix < tr.getChildCount(); ix++) {
 +                    getWrapperDiv(tr, ix).getStyle().clearProperty("display");
 +                }
 +            }
 +
 +        }
 +
 +        /**
 +         * This is the inverse of the RowExpandAnimation and is implemented by
 +         * extending it and overriding the calculation of offsets and heights.
 +         */
 +        private class RowCollapseAnimation extends RowExpandAnimation {
 +
 +            private final List<VScrollTableRow> rows;
 +
++            /**
++             * @param rows
++             *            List of rows to animate. Must not be empty.
++             */
 +            public RowCollapseAnimation(List<VScrollTableRow> rows) {
 +                super(rows);
 +                this.rows = rows;
 +            }
 +
 +            @Override
 +            protected String getInitialHeight() {
 +                return getRowHeight() + "px";
 +            }
 +
 +            @Override
 +            protected double getBaseOffset() {
 +                return getRowHeight();
 +            }
 +
 +            @Override
 +            protected double calculateHeightOfAllVisibleLines(double progress,
 +                    double rh) {
 +                return rows.size() * rh * (1 - progress);
 +            }
 +
 +            @Override
 +            protected double calculateDivOffset(double progress, double rh) {
 +                return -super.calculateDivOffset(progress, rh);
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Icons rendered into first actual column in TreeTable, not to row header
 +     * cell
 +     */
 +    @Override
 +    protected String buildCaptionHtmlSnippet(UIDL uidl) {
 +        if (uidl.getTag().equals("column")) {
 +            return super.buildCaptionHtmlSnippet(uidl);
 +        } else {
 +            String s = uidl.getStringAttribute("caption");
 +            return s;
 +        }
 +    }
 +
 +    @Override
 +    protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
 +        if (collapseRequest || focusParentResponsePending) {
 +            // Enqueue the event if there might be pending content changes from
 +            // the server
 +            if (pendingNavigationEvents.size() < 10) {
 +                // Only keep 10 keyboard events in the queue
 +                PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent(
 +                        keycode, ctrl, shift);
 +                pendingNavigationEvents.add(pendingNavigationEvent);
 +            }
 +            return true;
 +        }
 +
 +        VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow();
 +        if (focusedRow != null) {
 +            if (focusedRow.canHaveChildren
 +                    && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) {
 +                if (!ctrl) {
 +                    client.updateVariable(paintableId, "selectCollapsed", true,
 +                            false);
 +                }
 +                sendSelectedRows(false);
 +                sendToggleCollapsedUpdate(focusedRow.getKey());
 +                return true;
 +            } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) {
 +                // already expanded, move selection down if next is on a deeper
 +                // level (is-a-child)
 +                VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow
 +                        .getParent();
 +                Iterator<Widget> iterator = body.iterator();
 +                VTreeTableRow next = null;
 +                while (iterator.hasNext()) {
 +                    next = (VTreeTableRow) iterator.next();
 +                    if (next == focusedRow) {
 +                        next = (VTreeTableRow) iterator.next();
 +                        break;
 +                    }
 +                }
 +                if (next != null) {
 +                    if (next.depth > focusedRow.depth) {
 +                        selectionPending = true;
 +                        return super.handleNavigation(getNavigationDownKey(),
 +                                ctrl, shift);
 +                    }
 +                } else {
 +                    // Note, a minor change here for a bit false behavior if
 +                    // cache rows is disabled + last visible row + no childs for
 +                    // the node
 +                    selectionPending = true;
 +                    return super.handleNavigation(getNavigationDownKey(), ctrl,
 +                            shift);
 +                }
 +            } else if (keycode == KeyCodes.KEY_LEFT) {
 +                // already collapsed move selection up to parent node
 +                // do on the server side as the parent is not necessary
 +                // rendered on the client, could check if parent is visible if
 +                // a performance issue arises
 +
 +                client.updateVariable(paintableId, "focusParent",
 +                        focusedRow.getKey(), true);
 +
 +                // Set flag that we should enqueue navigation events until we
 +                // get a response to this request
 +                focusParentResponsePending = true;
 +
 +                return true;
 +            }
 +        }
 +        return super.handleNavigation(keycode, ctrl, shift);
 +    }
 +
 +    private void sendToggleCollapsedUpdate(String rowKey) {
 +        collapsedRowKey = rowKey;
 +        collapseRequest = true;
 +        client.updateVariable(paintableId, "toggleCollapsed", rowKey, true);
 +    }
 +
 +    @Override
 +    public void onBrowserEvent(Event event) {
 +        super.onBrowserEvent(event);
 +        if (event.getTypeInt() == Event.ONKEYUP && selectionPending) {
 +            sendSelectedRows();
 +        }
 +    }
 +
 +    @Override
 +    protected void sendSelectedRows(boolean immediately) {
 +        super.sendSelectedRows(immediately);
 +        selectionPending = false;
 +    }
 +
 +    @Override
 +    protected void reOrderColumn(String columnKey, int newIndex) {
 +        super.reOrderColumn(columnKey, newIndex);
 +        // current impl not intelligent enough to survive without visiting the
 +        // server to redraw content
 +        client.sendPendingVariableChanges();
 +    }
 +
 +    @Override
 +    public void setStyleName(String style) {
 +        super.setStyleName(style + " v-treetable");
 +    }
 +
 +    @Override
 +    protected void updateTotalRows(UIDL uidl) {
 +        // Make sure that initializedAndAttached & al are not reset when the
 +        // totalrows are updated on expand/collapse requests.
 +        int newTotalRows = uidl.getIntAttribute("totalrows");
 +        setTotalRows(newTotalRows);
 +    }
 +
 +}
index 23dee15359c438f5b94f108273ec1c39b4a8a151,6ada7975705b5b9301fab714f3509ef124322d20..7aef4a2b2a3745732c238e5db0b3efc33354bc49
@@@ -108,6 -115,6 +108,7 @@@ public class TabSheet extends AbstractC
          setWidth(100, UNITS_PERCENTAGE);
          setImmediate(true);
          setCloseHandler(new CloseHandler() {
++            @Override
              public void onTabClose(TabSheet tabsheet, Component c) {
                  tabsheet.removeComponent(c);
              }
       * 
       * @return the unmodifiable Iterator of the tab content components
       */
++    @Override
      public Iterator<Component> getComponentIterator() {
          return Collections.unmodifiableList(components).iterator();
      }
       * 
       * @return the number of contained components
       */
++    @Override
      public int getComponentCount() {
          return components.size();
      }
      }
  
      // inherits javadoc
+     @Override
      public void changeVariables(Object source, Map<String, Object> variables) {
          if (variables.containsKey("selected")) {
 -            setSelectedTab((Component) keyMapper.get((String) variables
 -                    .get("selected")));
 +            setSelectedTab(keyMapper.get((String) variables.get("selected")));
          }
          if (variables.containsKey("close")) {
 -            final Component tab = (Component) keyMapper.get((String) variables
 -                    .get("close"));
 +            final Component tab = keyMapper
 +                    .get((String) variables.get("close"));
              if (tab != null) {
                  closeHandler.onTabClose(this, tab);
              }
       * 
       * {@inheritDoc}
       */
++    @Override
      public void replaceComponent(Component oldComponent, Component newComponent) {
  
          if (selected == oldComponent) {
          /**
           * Returns the tab caption. Can never be null.
           */
++        @Override
          public String getCaption() {
              return caption;
          }
  
++        @Override
          public void setCaption(String caption) {
              this.caption = caption;
              requestRepaint();
          }
  
++        @Override
          public Resource getIcon() {
              return icon;
          }
  
++        @Override
          public void setIcon(Resource icon) {
              this.icon = icon;
              requestRepaint();
          }
  
++        @Override
          public boolean isEnabled() {
              return enabled;
          }
  
++        @Override
          public void setEnabled(boolean enabled) {
              this.enabled = enabled;
              if (updateSelection()) {
              requestRepaint();
          }
  
++        @Override
          public boolean isVisible() {
              return visible;
          }
  
++        @Override
          public void setVisible(boolean visible) {
              this.visible = visible;
              if (updateSelection()) {
              requestRepaint();
          }
  
++        @Override
          public boolean isClosable() {
              return closable;
          }
  
++        @Override
          public void setClosable(boolean closable) {
              this.closable = closable;
              requestRepaint();
  
          }
  
++        @Override
          public String getDescription() {
              return description;
          }
  
++        @Override
          public void setDescription(String description) {
              this.description = description;
              requestRepaint();
          }
  
++        @Override
          public ErrorMessage getComponentError() {
              return componentError;
          }
  
++        @Override
          public void setComponentError(ErrorMessage componentError) {
              this.componentError = componentError;
              requestRepaint();
          }
  
++        @Override
          public Component getComponent() {
              for (Map.Entry<Component, Tab> entry : tabs.entrySet()) {
                  if (entry.getValue() == this) {
              return null;
          }
  
++        @Override
          public void setStyleName(String styleName) {
              this.styleName = styleName;
              requestRepaint();
          }
  
++        @Override
          public String getStyleName() {
              return styleName;
          }
          super.focus();
      }
  
++    @Override
      public int getTabIndex() {
          return tabIndex;
      }
  
++    @Override
      public void setTabIndex(int tabIndex) {
          this.tabIndex = tabIndex;
          requestRepaint();
      }
  
++    @Override
      public void addListener(BlurListener listener) {
          addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
                  BlurListener.blurMethod);
      }
  
++    @Override
      public void removeListener(BlurListener listener) {
          removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
      }
  
++    @Override
      public void addListener(FocusListener listener) {
          addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
                  FocusListener.focusMethod);
      }
  
++    @Override
      public void removeListener(FocusListener listener) {
          removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
  
      }
  
 +    @Override
 +    public boolean isComponentVisible(Component childComponent) {
 +        return childComponent == getSelectedTab();
 +    }
++
+     /**
+      * Copies properties from one Tab to another.
+      * 
+      * @param from
+      *            The tab whose data to copy.
+      * @param to
+      *            The tab to which copy the data.
+      */
+     private static void copyTabMetadata(Tab from, Tab to) {
+         to.setCaption(from.getCaption());
+         to.setIcon(from.getIcon());
+         to.setDescription(from.getDescription());
+         to.setVisible(from.isVisible());
+         to.setEnabled(from.isEnabled());
+         to.setClosable(from.isClosable());
+         to.setStyleName(from.getStyleName());
+         to.setComponentError(from.getComponentError());
+     }
  }
Simple merge