]> source.dussan.org Git - vaadin-framework.git/commitdiff
Merge branch '6.8'
authorJohannes Dahlström <johannesd@vaadin.com>
Tue, 24 Jul 2012 12:04:49 +0000 (15:04 +0300)
committerJohannes Dahlström <johannesd@vaadin.com>
Tue, 24 Jul 2012 12:04:49 +0000 (15:04 +0300)
Conflicts:
WebContent/VAADIN/themes/base/treetable/treetable.css
WebContent/release-notes.html
src/com/vaadin/terminal/gwt/client/VUIDLBrowser.java
src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java

1  2 
WebContent/VAADIN/themes/base/table/table.css
WebContent/VAADIN/themes/base/treetable/treetable.css
WebContent/VAADIN/themes/reindeer/table/table.css
src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java
src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java
src/com/vaadin/ui/Table.java
tests/testbench/com/vaadin/tests/components/treetable/AddNodesOnExpand.java

index de505ba774dfa8b377b5480603d371d3d3410f63,662839a4a29acac2c71948446f20e56044d60fb8..4d9ad5031b0bfa6a8094b69ee6666fb96a10e020
  }
  
  .v-treetable-node-closed {
-     background: url(../treetable/img/arrow-right.png) right center no-repeat;
+     background: url(../treetable/img/arrow-right.png) right top no-repeat;
  }
  
 -.v-ie6 .v-treetable-node-closed {
 -      background-image: url(../treetable/img/arrow-right.gif); 
 -} 
 -
  .v-treetable-node-open {
-     background: url(../treetable/img/arrow-down.png) right center no-repeat;
+     background: url(../treetable/img/arrow-down.png) right top no-repeat;
  }
  
 -.v-ie6 .v-treetable-node-open {
 -      background-image: url(../treetable/img/arrow-down.gif); 
 -} 
 -
  .v-treetable .v-checkbox {
        display: inline-block;
        padding-bottom: 4px;
index 4050f1bafcb531f790be4167182b2afb9cc8f5b5,0000000000000000000000000000000000000000..c8bebc2c6659cbbf78f007e179bf34a03cf3c371
mode 100644,000000..100644
--- /dev/null
@@@ -1,408 -1,0 +1,413 @@@
-                 client.updateVariable(paintableId, "text", getText(), false);
 +/*
 +@VaadinApache2LicenseForJavaFiles@
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.textfield;
 +
 +import com.google.gwt.event.dom.client.BlurEvent;
 +import com.google.gwt.event.dom.client.BlurHandler;
 +import com.google.gwt.event.dom.client.ChangeEvent;
 +import com.google.gwt.event.dom.client.ChangeHandler;
 +import com.google.gwt.event.dom.client.FocusEvent;
 +import com.google.gwt.event.dom.client.FocusHandler;
 +import com.google.gwt.event.dom.client.KeyCodes;
 +import com.google.gwt.event.dom.client.KeyDownEvent;
 +import com.google.gwt.event.dom.client.KeyDownHandler;
 +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.Timer;
 +import com.google.gwt.user.client.ui.TextBoxBase;
 +import com.vaadin.terminal.gwt.client.ApplicationConnection;
 +import com.vaadin.terminal.gwt.client.BrowserInfo;
 +import com.vaadin.terminal.gwt.client.EventId;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.ui.Field;
 +
 +/**
 + * This class represents a basic text input field with one row.
 + * 
 + * @author Vaadin Ltd.
 + * 
 + */
 +public class VTextField extends TextBoxBase implements Field, ChangeHandler,
 +        FocusHandler, BlurHandler, KeyDownHandler {
 +
 +    public static final String VAR_CUR_TEXT = "curText";
 +
 +    public static final String ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS = "nvc";
 +    /**
 +     * The input node CSS classname.
 +     */
 +    public static final String CLASSNAME = "v-textfield";
 +    /**
 +     * This CSS classname is added to the input node on hover.
 +     */
 +    public static final String CLASSNAME_FOCUS = "focus";
 +
 +    protected String paintableId;
 +
 +    protected ApplicationConnection client;
 +
 +    protected String valueBeforeEdit = null;
 +
 +    /**
 +     * Set to false if a text change event has been sent since the last value
 +     * change event. This means that {@link #valueBeforeEdit} should not be
 +     * trusted when determining whether a text change even should be sent.
 +     */
 +    private boolean valueBeforeEditIsSynced = true;
 +
 +    private boolean immediate = false;
 +    private int maxLength = -1;
 +
 +    private static final String CLASSNAME_PROMPT = "prompt";
 +    public static final String ATTR_TEXTCHANGE_TIMEOUT = "iet";
 +    public static final String VAR_CURSOR = "c";
 +    public static final String ATTR_TEXTCHANGE_EVENTMODE = "iem";
 +    protected static final String TEXTCHANGE_MODE_EAGER = "EAGER";
 +    private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT";
 +
 +    private String inputPrompt = null;
 +    private boolean prompting = false;
 +    private int lastCursorPos = -1;
 +
 +    public VTextField() {
 +        this(DOM.createInputText());
 +    }
 +
 +    protected VTextField(Element node) {
 +        super(node);
 +        setStyleName(CLASSNAME);
 +        addChangeHandler(this);
 +        if (BrowserInfo.get().isIE()) {
 +            // IE does not send change events when pressing enter in a text
 +            // input so we handle it using a key listener instead
 +            addKeyDownHandler(this);
 +        }
 +        addFocusHandler(this);
 +        addBlurHandler(this);
 +    }
 +
 +    /*
 +     * TODO When GWT adds ONCUT, add it there and remove workaround. See
 +     * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030
 +     * 
 +     * Also note that the cut/paste are not totally crossbrowsers compatible.
 +     * E.g. in Opera mac works via context menu, but on via File->Paste/Cut.
 +     * Opera might need the polling method for 100% working textchanceevents.
 +     * Eager polling for a change is bit dum and heavy operation, so I guess we
 +     * should first try to survive without.
 +     */
 +    protected static final int TEXTCHANGE_EVENTS = Event.ONPASTE
 +            | Event.KEYEVENTS | Event.ONMOUSEUP;
 +
 +    @Override
 +    public void onBrowserEvent(Event event) {
 +        super.onBrowserEvent(event);
 +
 +        if (listenTextChangeEvents
 +                && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event
 +                        .getTypeInt()) {
 +            deferTextChangeEvent();
 +        }
 +
 +    }
 +
 +    /*
 +     * TODO optimize this so that only changes are sent + make the value change
 +     * event just a flag that moves the current text to value
 +     */
 +    private String lastTextChangeString = null;
 +
 +    private String getLastCommunicatedString() {
 +        return lastTextChangeString;
 +    }
 +
 +    private void communicateTextValueToServer() {
 +        String text = getText();
 +        if (prompting) {
 +            // Input prompt visible, text is actually ""
 +            text = "";
 +        }
 +        if (!text.equals(getLastCommunicatedString())) {
 +            if (valueBeforeEditIsSynced && text.equals(valueBeforeEdit)) {
 +                /*
 +                 * Value change for the current text has been enqueued since the
 +                 * last text change event was sent, but we can't know that it
 +                 * has been sent to the server. Ensure that all pending changes
 +                 * are sent now. Sending a value change without a text change
 +                 * will simulate a TextChangeEvent on the server.
 +                 */
 +                client.sendPendingVariableChanges();
 +            } else {
 +                // Default case - just send an immediate text change message
 +                client.updateVariable(paintableId, VAR_CUR_TEXT, text, true);
 +
 +                // Shouldn't investigate valueBeforeEdit to avoid duplicate text
 +                // change events as the states are not in sync any more
 +                valueBeforeEditIsSynced = false;
 +            }
 +            lastTextChangeString = text;
 +        }
 +    }
 +
 +    private Timer textChangeEventTrigger = new Timer() {
 +
 +        @Override
 +        public void run() {
 +            if (isAttached()) {
 +                updateCursorPosition();
 +                communicateTextValueToServer();
 +                scheduled = false;
 +            }
 +        }
 +    };
 +    private boolean scheduled = false;
 +    protected boolean listenTextChangeEvents;
 +    protected String textChangeEventMode;
 +    protected int textChangeEventTimeout;
 +
 +    private void deferTextChangeEvent() {
 +        if (textChangeEventMode.equals(TEXTCHANGE_MODE_TIMEOUT) && scheduled) {
 +            return;
 +        } else {
 +            textChangeEventTrigger.cancel();
 +        }
 +        textChangeEventTrigger.schedule(getTextChangeEventTimeout());
 +        scheduled = true;
 +    }
 +
 +    private int getTextChangeEventTimeout() {
 +        return textChangeEventTimeout;
 +    }
 +
 +    @Override
 +    public void setReadOnly(boolean readOnly) {
 +        boolean wasReadOnly = isReadOnly();
 +
 +        if (readOnly) {
 +            setTabIndex(-1);
 +        } else if (wasReadOnly && !readOnly && getTabIndex() == -1) {
 +            /*
 +             * Need to manually set tab index to 0 since server will not send
 +             * the tab index if it is 0.
 +             */
 +            setTabIndex(0);
 +        }
 +
 +        super.setReadOnly(readOnly);
 +    }
 +
 +    protected void updateFieldContent(final String text) {
 +        setPrompting(inputPrompt != null && focusedTextField != this
 +                && (text.equals("")));
 +
 +        String fieldValue;
 +        if (prompting) {
 +            fieldValue = isReadOnly() ? "" : inputPrompt;
 +            addStyleDependentName(CLASSNAME_PROMPT);
 +        } else {
 +            fieldValue = text;
 +            removeStyleDependentName(CLASSNAME_PROMPT);
 +        }
 +        setText(fieldValue);
 +
 +        lastTextChangeString = valueBeforeEdit = text;
 +        valueBeforeEditIsSynced = true;
 +    }
 +
 +    protected void onCut() {
 +        if (listenTextChangeEvents) {
 +            deferTextChangeEvent();
 +        }
 +    }
 +
 +    protected native void attachCutEventListener(Element el)
 +    /*-{
 +        var me = this;
 +        el.oncut = $entry(function() {
 +            me.@com.vaadin.terminal.gwt.client.ui.textfield.VTextField::onCut()();
 +        });
 +    }-*/;
 +
 +    protected native void detachCutEventListener(Element el)
 +    /*-{
 +        el.oncut = null;
 +    }-*/;
 +
 +    @Override
 +    protected void onDetach() {
 +        super.onDetach();
 +        detachCutEventListener(getElement());
 +        if (focusedTextField == this) {
 +            focusedTextField = null;
 +        }
 +    }
 +
 +    @Override
 +    protected void onAttach() {
 +        super.onAttach();
 +        if (listenTextChangeEvents) {
 +            detachCutEventListener(getElement());
 +        }
 +    }
 +
 +    protected void setMaxLength(int newMaxLength) {
 +        if (newMaxLength >= 0) {
 +            maxLength = newMaxLength;
 +        } else {
 +            maxLength = -1;
 +        }
 +        setMaxLengthToElement(newMaxLength);
 +    }
 +
 +    protected void setMaxLengthToElement(int newMaxLength) {
 +        if (newMaxLength >= 0) {
 +            getElement().setPropertyInt("maxLength", newMaxLength);
 +        } else {
 +            getElement().removeAttribute("maxLength");
 +        }
 +    }
 +
 +    public int getMaxLength() {
 +        return maxLength;
 +    }
 +
 +    @Override
 +    public void onChange(ChangeEvent event) {
 +        valueChange(false);
 +    }
 +
 +    /**
 +     * Called when the field value might have changed and/or the field was
 +     * blurred. These are combined so the blur event is sent in the same batch
 +     * as a possible value change event (these are often connected).
 +     * 
 +     * @param blurred
 +     *            true if the field was blurred
 +     */
 +    public void valueChange(boolean blurred) {
 +        if (client != null && paintableId != null) {
 +            boolean sendBlurEvent = false;
 +            boolean sendValueChange = false;
 +
 +            if (blurred && client.hasEventListeners(this, EventId.BLUR)) {
 +                sendBlurEvent = true;
 +                client.updateVariable(paintableId, EventId.BLUR, "", false);
 +            }
 +
 +            String newText = getText();
 +            if (!prompting && newText != null
 +                    && !newText.equals(valueBeforeEdit)) {
 +                sendValueChange = immediate;
++                client.updateVariable(paintableId, "text", newText, false);
 +                valueBeforeEdit = newText;
 +                valueBeforeEditIsSynced = true;
 +            }
 +
 +            /*
 +             * also send cursor position, no public api yet but for easier
 +             * extension
 +             */
 +            updateCursorPosition();
 +
 +            if (sendBlurEvent || sendValueChange) {
 +                /*
 +                 * Avoid sending text change event as we will simulate it on the
 +                 * server side before value change events.
 +                 */
 +                textChangeEventTrigger.cancel();
 +                scheduled = false;
 +                client.sendPendingVariableChanges();
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Updates the cursor position variable if it has changed since the last
 +     * update.
 +     * 
 +     * @return true iff the value was updated
 +     */
 +    protected boolean updateCursorPosition() {
 +        if (Util.isAttachedAndDisplayed(this)) {
 +            int cursorPos = getCursorPos();
 +            if (lastCursorPos != cursorPos) {
 +                client.updateVariable(paintableId, VAR_CURSOR, cursorPos, false);
 +                lastCursorPos = cursorPos;
 +                return true;
 +            }
 +        }
 +        return false;
 +    }
 +
 +    private static VTextField focusedTextField;
 +
 +    public static void flushChangesFromFocusedTextField() {
 +        if (focusedTextField != null) {
 +            focusedTextField.onChange(null);
 +        }
 +    }
 +
 +    @Override
 +    public void onFocus(FocusEvent event) {
 +        addStyleDependentName(CLASSNAME_FOCUS);
 +        if (prompting) {
 +            setText("");
 +            removeStyleDependentName(CLASSNAME_PROMPT);
 +            setPrompting(false);
 +        }
 +        focusedTextField = this;
 +        if (client.hasEventListeners(this, EventId.FOCUS)) {
 +            client.updateVariable(paintableId, EventId.FOCUS, "", true);
 +        }
 +    }
 +
 +    @Override
 +    public void onBlur(BlurEvent event) {
++        // this is called twice on Chrome when e.g. changing tab while prompting
++        // field focused - do not change settings on the second time
++        if (focusedTextField != this) {
++            return;
++        }
 +        removeStyleDependentName(CLASSNAME_FOCUS);
 +        focusedTextField = null;
 +        String text = getText();
 +        setPrompting(inputPrompt != null && (text == null || "".equals(text)));
 +        if (prompting) {
 +            setText(isReadOnly() ? "" : inputPrompt);
 +            addStyleDependentName(CLASSNAME_PROMPT);
 +        }
 +
 +        valueChange(true);
 +    }
 +
 +    private void setPrompting(boolean prompting) {
 +        this.prompting = prompting;
 +    }
 +
 +    public void setColumns(int columns) {
 +        if (columns <= 0) {
 +            return;
 +        }
 +
 +        setWidth(columns + "em");
 +    }
 +
 +    @Override
 +    public void onKeyDown(KeyDownEvent event) {
 +        if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
 +            valueChange(false);
 +        }
 +    }
 +
 +    public void setImmediate(boolean immediate) {
 +        this.immediate = immediate;
 +    }
 +
 +    public void setInputPrompt(String inputPrompt) {
 +        this.inputPrompt = inputPrompt;
 +    }
 +
 +}
index 91e1fe2a3dcdba7b65ce88cbeda4c7b5152166be,0000000000000000000000000000000000000000..c03dff950729634f152b81d6060347fa5f7db289
mode 100644,000000..100644
--- /dev/null
@@@ -1,816 -1,0 +1,830 @@@
-         private int identWidth = -1;
 +/*
 +@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 {
-                     setIdent();
++        private int indentWidth = -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;
-             private void setIdent() {
-                 if (getIdentWidth() > 0 && depth != 0) {
-                     treeSpacer.getStyle().setWidth(
-                             (depth + 1) * getIdentWidth(), Unit.PX);
++                    setIndent();
 +                    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;
 +                }
 +
 +            }
 +
-                 if (getIdentWidth() < 0) {
-                     detectIdent(this);
++            private void setIndent() {
++                if (getIndentWidth() > 0) {
++                    treeSpacer.getParentElement().getStyle()
++                            .setPaddingLeft(getIndent(), Unit.PX);
++                    treeSpacer.getStyle().setWidth(getIndent(), Unit.PX);
 +                }
 +            }
 +
 +            @Override
 +            protected void onAttach() {
 +                super.onAttach();
-         private int getIdentWidth() {
-             return identWidth;
++                if (getIndentWidth() < 0) {
++                    detectIndent(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;
 +            }
 +
++            @Override
++            protected void setCellWidth(int cellIx, int width) {
++                if (cellIx == colIndexOfHierarchy + (showRowHeaders ? 1 : 0)) {
++                    // take indentation padding into account if this is the
++                    // hierarchy column
++                    width = Math.max(width - getIndent(), 0);
++                }
++                super.setCellWidth(cellIx, width);
++            }
++
++            private int getIndent() {
++                return (depth + 1) * getIndentWidth();
++            }
 +        }
 +
 +        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() {
 +
 +                    @Override
 +                    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 void detectIdent(VTreeTableRow vTreeTableRow) {
-             identWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
-             if (identWidth == 0) {
-                 identWidth = -1;
++        private int getIndentWidth() {
++            return indentWidth;
 +        }
 +
-                 next.setIdent();
++        private void detectIndent(VTreeTableRow vTreeTableRow) {
++            indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
++            if (indentWidth == 0) {
++                indentWidth = -1;
 +                return;
 +            }
 +            Iterator<Widget> iterator = iterator();
 +            while (iterator.hasNext()) {
 +                VTreeTableRow next = (VTreeTableRow) iterator.next();
++                next.setIndent();
 +            }
 +        }
 +
 +        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);
 +                }
 +            }
 +            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);
 +    }
 +
 +}
Simple merge