]> source.dussan.org Git - vaadin-framework.git/commitdiff
Merge remote branch 'origin/6.8'
authorLeif Åstrand <leif@vaadin.com>
Wed, 25 Apr 2012 11:31:58 +0000 (14:31 +0300)
committerLeif Åstrand <leif@vaadin.com>
Wed, 25 Apr 2012 11:31:58 +0000 (14:31 +0300)
Conflicts:
src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
src/com/vaadin/terminal/gwt/client/HistoryImplIEVaadin.java
src/com/vaadin/terminal/gwt/client/Util.java
src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java
src/com/vaadin/terminal/gwt/client/ui/UploadIFrameOnloadStrategy.java
src/com/vaadin/terminal/gwt/client/ui/UploadIFrameOnloadStrategyIE.java
src/com/vaadin/terminal/gwt/client/ui/VCustomLayout.java
src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java
src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapperIE.java
src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java
src/com/vaadin/terminal/gwt/client/ui/VTabsheet.java
src/com/vaadin/terminal/gwt/client/ui/VTextField.java
src/com/vaadin/terminal/gwt/client/ui/VVideo.java
src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java
tests/testbench/com/vaadin/tests/components/table/TestCurrentPageFirstItem.java

16 files changed:
1  2 
build/build.xml
src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
src/com/vaadin/terminal/gwt/client/VDebugConsole.java
src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java
src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java
src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java
src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java
src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java
src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java
src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java
src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java
src/com/vaadin/terminal/gwt/server/RequestTimer.java
tests/testbench/com/vaadin/tests/components/TouchScrollables.java
tests/testbench/com/vaadin/tests/components/table/TestCurrentPageFirstItem.java

diff --cc build/build.xml
Simple merge
index 2be35f053fc0d57d166f291486942f1ffcd5ec22,de7ad83b54f78bca8023f52cc1137459e1752891..be6d5781129748570a2d0cd9e436eaa20efadebf
@@@ -256,24 -220,38 +256,33 @@@ public class ApplicationConnection 
      /*-{
        var ap = this;
        var client = {};
-       client.isActive = function() {
+       client.isActive = $entry(function() {
                return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()()
                                || ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::isExecutingDeferredCommands()();
-       }
+       });
 -      
        var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()();
        if (vi) {
                client.getVersionInfo = function() {
                        return vi;
                }
        }
 -      
 +
-       client.getElementByPath = function(id) {
+       client.getProfilingData = $entry(function() {
+           var pd = [
+                   ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::lastProcessingTime,
+                     ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::totalProcessingTime
+               ];
+           pd = pd.concat(ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::testBenchServerStatus);
+           return pd;
+       });
+       client.getElementByPath = $entry(function(id) {
                return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
-       }
-       client.getPathForElement = function(element) {
+       });
+       client.getPathForElement = $entry(function(element) {
                return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
-       }
+       });
  
 -      if (!$wnd.vaadin.clients) {
 -              $wnd.vaadin.clients = {};
 -      }
 -
        $wnd.vaadin.clients[TTAppId] = client;
      }-*/;
  
                      json.getValueMap("typeMappings"), widgetSet);
          }
  
 +        handleUIDLDuration.logDuration(
 +                " * Handling type mappings from server completed", 10);
+         /*
+          * Hook for TestBench to get details about server status
+          */
+         if (json.containsKey("tbss")) {
+             testBenchServerStatus = json.getValueMap("tbss");
+         }
  
          Command c = new Command() {
              public void execute() {
  
                  // TODO build profiling for widget impl loading time
  
-                 final long prosessingTime = (new Date().getTime())
-                         - start.getTime();
+                 lastProcessingTime = (int) ((new Date().getTime()) - start
+                         .getTime());
+                 totalProcessingTime += lastProcessingTime;
                  VConsole.log(" Processing time was "
-                         + String.valueOf(prosessingTime) + "ms for "
+                         + String.valueOf(lastProcessingTime) + "ms for "
                          + jsonText.length() + " characters of JSON");
 -                VConsole.log("Referenced paintables: "
 -                        + idToPaintableDetail.size());
 +                VConsole.log("Referenced paintables: " + connectorMap.size());
  
                  endRequest();
  
index 6b22f3c9f3a78f86808f8e71410d44ba56b398f8,96cb4b8a3533a96f81940a2fbc72332202952a07..533d6a78ae9d36c9409833c32cd1c1427d60abde
@@@ -21,6 -23,8 +23,7 @@@ import com.google.gwt.user.client.Event
  import com.google.gwt.user.client.ui.ScrollPanel;
  import com.google.gwt.user.client.ui.Widget;
  import com.google.gwt.user.client.ui.impl.FocusImpl;
 -import com.vaadin.terminal.gwt.client.VConsole;
+ import com.vaadin.terminal.gwt.client.BrowserInfo;
  
  /**
   * A scrollhandlers similar to {@link ScrollPanel}.
index 5208d7cacfe991c40f2d7b984192cce80df26233,0000000000000000000000000000000000000000..b4194c40a6b608761c4683fdbfecc84e4e01c9d9
mode 100644,000000..100644
--- /dev/null
@@@ -1,408 -1,0 +1,408 @@@
-       element.notifyChildrenOfSizeChange = function() {
 +/* 
 +@VaadinApache2LicenseForJavaFiles@
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.customlayout;
 +
 +import java.util.HashMap;
 +import java.util.Iterator;
 +
 +import com.google.gwt.dom.client.ImageElement;
 +import com.google.gwt.dom.client.NodeList;
 +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.ComplexPanel;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.terminal.gwt.client.ApplicationConnection;
 +import com.vaadin.terminal.gwt.client.BrowserInfo;
 +import com.vaadin.terminal.gwt.client.ComponentConnector;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.VCaption;
 +import com.vaadin.terminal.gwt.client.VCaptionWrapper;
 +
 +/**
 + * Custom Layout implements complex layout defined with HTML template.
 + * 
 + * @author Vaadin Ltd
 + * 
 + */
 +public class VCustomLayout extends ComplexPanel {
 +
 +    public static final String CLASSNAME = "v-customlayout";
 +
 +    /** Location-name to containing element in DOM map */
 +    private final HashMap<String, Element> locationToElement = new HashMap<String, Element>();
 +
 +    /** Location-name to contained widget map */
 +    final HashMap<String, Widget> locationToWidget = new HashMap<String, Widget>();
 +
 +    /** Widget to captionwrapper map */
 +    private final HashMap<Widget, VCaptionWrapper> childWidgetToCaptionWrapper = new HashMap<Widget, VCaptionWrapper>();
 +
 +    /** Name of the currently rendered style */
 +    String currentTemplateName;
 +
 +    /** Unexecuted scripts loaded from the template */
 +    String scripts = "";
 +
 +    /** Paintable ID of this paintable */
 +    String pid;
 +
 +    ApplicationConnection client;
 +
 +    private boolean htmlInitialized = false;
 +
 +    private Element elementWithNativeResizeFunction;
 +
 +    private String height = "";
 +
 +    private String width = "";
 +
 +    public VCustomLayout() {
 +        setElement(DOM.createDiv());
 +        // Clear any unwanted styling
 +        DOM.setStyleAttribute(getElement(), "border", "none");
 +        DOM.setStyleAttribute(getElement(), "margin", "0");
 +        DOM.setStyleAttribute(getElement(), "padding", "0");
 +
 +        if (BrowserInfo.get().isIE()) {
 +            DOM.setStyleAttribute(getElement(), "position", "relative");
 +        }
 +
 +        setStyleName(CLASSNAME);
 +    }
 +
 +    /**
 +     * Sets widget to given location.
 +     * 
 +     * If location already contains a widget it will be removed.
 +     * 
 +     * @param widget
 +     *            Widget to be set into location.
 +     * @param location
 +     *            location name where widget will be added
 +     * 
 +     * @throws IllegalArgumentException
 +     *             if no such location is found in the layout.
 +     */
 +    public void setWidget(Widget widget, String location) {
 +
 +        if (widget == null) {
 +            return;
 +        }
 +
 +        // If no given location is found in the layout, and exception is throws
 +        Element elem = locationToElement.get(location);
 +        if (elem == null && hasTemplate()) {
 +            throw new IllegalArgumentException("No location " + location
 +                    + " found");
 +        }
 +
 +        // Get previous widget
 +        final Widget previous = locationToWidget.get(location);
 +        // NOP if given widget already exists in this location
 +        if (previous == widget) {
 +            return;
 +        }
 +
 +        if (previous != null) {
 +            remove(previous);
 +        }
 +
 +        // if template is missing add element in order
 +        if (!hasTemplate()) {
 +            elem = getElement();
 +        }
 +
 +        // Add widget to location
 +        super.add(widget, elem);
 +        locationToWidget.put(location, widget);
 +    }
 +
 +    /** Initialize HTML-layout. */
 +    public void initializeHTML(String template, String themeUri) {
 +
 +        // Connect body of the template to DOM
 +        template = extractBodyAndScriptsFromTemplate(template);
 +
 +        // TODO prefix img src:s here with a regeps, cannot work further with IE
 +
 +        String relImgPrefix = themeUri + "/layouts/";
 +
 +        // prefix all relative image elements to point to theme dir with a
 +        // regexp search
 +        template = template.replaceAll(
 +                "<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"",
 +                "<$1 $2src=\"" + relImgPrefix + "$3\"");
 +        // also support src attributes without quotes
 +        template = template
 +                .replaceAll(
 +                        "<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]",
 +                        "<$1 $2src=\"" + relImgPrefix + "$3\"");
 +        // also prefix relative style="...url(...)..."
 +        template = template
 +                .replaceAll(
 +                        "(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)",
 +                        "$1 " + relImgPrefix + "$2 $3");
 +
 +        getElement().setInnerHTML(template);
 +
 +        // Remap locations to elements
 +        locationToElement.clear();
 +        scanForLocations(getElement());
 +
 +        initImgElements();
 +
 +        elementWithNativeResizeFunction = DOM.getFirstChild(getElement());
 +        if (elementWithNativeResizeFunction == null) {
 +            elementWithNativeResizeFunction = getElement();
 +        }
 +        publishResizedFunction(elementWithNativeResizeFunction);
 +
 +        htmlInitialized = true;
 +    }
 +
 +    private native boolean uriEndsWithSlash()
 +    /*-{
 +        var path =  $wnd.location.pathname;
 +        if(path.charAt(path.length - 1) == "/")
 +            return true;
 +        return false;
 +    }-*/;
 +
 +    boolean hasTemplate() {
 +        return htmlInitialized;
 +    }
 +
 +    /** Collect locations from template */
 +    private void scanForLocations(Element elem) {
 +
 +        final String location = elem.getAttribute("location");
 +        if (!"".equals(location)) {
 +            locationToElement.put(location, elem);
 +            elem.setInnerHTML("");
 +
 +        } else {
 +            final int len = DOM.getChildCount(elem);
 +            for (int i = 0; i < len; i++) {
 +                scanForLocations(DOM.getChild(elem, i));
 +            }
 +        }
 +    }
 +
 +    /** Evaluate given script in browser document */
 +    static native void eval(String script)
 +    /*-{
 +      try {
 +       if (script != null) 
 +      eval("{ var document = $doc; var window = $wnd; "+ script + "}");
 +      } catch (e) {
 +      }
 +    }-*/;
 +
 +    /**
 +     * Img elements needs some special handling in custom layout. Img elements
 +     * will get their onload events sunk. This way custom layout can notify
 +     * parent about possible size change.
 +     */
 +    private void initImgElements() {
 +        NodeList<com.google.gwt.dom.client.Element> nodeList = getElement()
 +                .getElementsByTagName("IMG");
 +        for (int i = 0; i < nodeList.getLength(); i++) {
 +            com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList
 +                    .getItem(i);
 +            DOM.sinkEvents((Element) img.cast(), Event.ONLOAD);
 +        }
 +    }
 +
 +    /**
 +     * Extract body part and script tags from raw html-template.
 +     * 
 +     * Saves contents of all script-tags to private property: scripts. Returns
 +     * contents of the body part for the html without script-tags. Also replaces
 +     * all _UID_ tags with an unique id-string.
 +     * 
 +     * @param html
 +     *            Original HTML-template received from server
 +     * @return html that is used to create the HTMLPanel.
 +     */
 +    private String extractBodyAndScriptsFromTemplate(String html) {
 +
 +        // Replace UID:s
 +        html = html.replaceAll("_UID_", pid + "__");
 +
 +        // Exctract script-tags
 +        scripts = "";
 +        int endOfPrevScript = 0;
 +        int nextPosToCheck = 0;
 +        String lc = html.toLowerCase();
 +        String res = "";
 +        int scriptStart = lc.indexOf("<script", nextPosToCheck);
 +        while (scriptStart > 0) {
 +            res += html.substring(endOfPrevScript, scriptStart);
 +            scriptStart = lc.indexOf(">", scriptStart);
 +            final int j = lc.indexOf("</script>", scriptStart);
 +            scripts += html.substring(scriptStart + 1, j) + ";";
 +            nextPosToCheck = endOfPrevScript = j + "</script>".length();
 +            scriptStart = lc.indexOf("<script", nextPosToCheck);
 +        }
 +        res += html.substring(endOfPrevScript);
 +
 +        // Extract body
 +        html = res;
 +        lc = html.toLowerCase();
 +        int startOfBody = lc.indexOf("<body");
 +        if (startOfBody < 0) {
 +            res = html;
 +        } else {
 +            res = "";
 +            startOfBody = lc.indexOf(">", startOfBody) + 1;
 +            final int endOfBody = lc.indexOf("</body>", startOfBody);
 +            if (endOfBody > startOfBody) {
 +                res = html.substring(startOfBody, endOfBody);
 +            } else {
 +                res = html.substring(startOfBody);
 +            }
 +        }
 +
 +        return res;
 +    }
 +
 +    /** Update caption for given widget */
 +    public void updateCaption(ComponentConnector paintable) {
 +        Widget widget = paintable.getWidget();
 +        VCaptionWrapper wrapper = childWidgetToCaptionWrapper.get(widget);
 +        if (VCaption.isNeeded(paintable.getState())) {
 +            if (wrapper == null) {
 +                // Add a wrapper between the layout and the child widget
 +                final String loc = getLocation(widget);
 +                super.remove(widget);
 +                wrapper = new VCaptionWrapper(paintable, client);
 +                super.add(wrapper, locationToElement.get(loc));
 +                childWidgetToCaptionWrapper.put(widget, wrapper);
 +            }
 +            wrapper.updateCaption();
 +        } else {
 +            if (wrapper != null) {
 +                // Remove the wrapper and add the widget directly to the layout
 +                final String loc = getLocation(widget);
 +                super.remove(wrapper);
 +                super.add(widget, locationToElement.get(loc));
 +                childWidgetToCaptionWrapper.remove(widget);
 +            }
 +        }
 +    }
 +
 +    /** Get the location of an widget */
 +    public String getLocation(Widget w) {
 +        for (final Iterator<String> i = locationToWidget.keySet().iterator(); i
 +                .hasNext();) {
 +            final String location = i.next();
 +            if (locationToWidget.get(location) == w) {
 +                return location;
 +            }
 +        }
 +        return null;
 +    }
 +
 +    /** Removes given widget from the layout */
 +    @Override
 +    public boolean remove(Widget w) {
 +        final String location = getLocation(w);
 +        if (location != null) {
 +            locationToWidget.remove(location);
 +        }
 +        final VCaptionWrapper cw = childWidgetToCaptionWrapper.get(w);
 +        if (cw != null) {
 +            childWidgetToCaptionWrapper.remove(w);
 +            return super.remove(cw);
 +        } else if (w != null) {
 +            return super.remove(w);
 +        }
 +        return false;
 +    }
 +
 +    /** Adding widget without specifying location is not supported */
 +    @Override
 +    public void add(Widget w) {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    /** Clear all widgets from the layout */
 +    @Override
 +    public void clear() {
 +        super.clear();
 +        locationToWidget.clear();
 +        childWidgetToCaptionWrapper.clear();
 +    }
 +
 +    /**
 +     * This method is published to JS side with the same name into first DOM
 +     * node of custom layout. This way if one implements some resizeable
 +     * containers in custom layout he/she can notify children after resize.
 +     */
 +    public void notifyChildrenOfSizeChange() {
 +        client.runDescendentsLayout(this);
 +    }
 +
 +    @Override
 +    public void onDetach() {
 +        super.onDetach();
 +        if (elementWithNativeResizeFunction != null) {
 +            detachResizedFunction(elementWithNativeResizeFunction);
 +        }
 +    }
 +
 +    private native void detachResizedFunction(Element element)
 +    /*-{
 +      element.notifyChildrenOfSizeChange = null;
 +    }-*/;
 +
 +    private native void publishResizedFunction(Element element)
 +    /*-{
 +      var self = this;
-       };
++      element.notifyChildrenOfSizeChange = $entry(function() {
 +              self.@com.vaadin.terminal.gwt.client.ui.customlayout.VCustomLayout::notifyChildrenOfSizeChange()();
++      });
 +    }-*/;
 +
 +    /**
 +     * In custom layout one may want to run layout functions made with
 +     * JavaScript. This function tests if one exists (with name "iLayoutJS" in
 +     * layouts first DOM node) and runs et. Return value is used to determine if
 +     * children needs to be notified of size changes.
 +     * 
 +     * Note! When implementing a JS layout function you most likely want to call
 +     * notifyChildrenOfSizeChange() function on your custom layouts main
 +     * element. That method is used to control whether child components layout
 +     * functions are to be run.
 +     * 
 +     * @param el
 +     * @return true if layout function exists and was run successfully, else
 +     *         false.
 +     */
 +    native boolean iLayoutJS(Element el)
 +    /*-{
 +      if(el && el.iLayoutJS) {
 +              try {
 +                      el.iLayoutJS();
 +                      return true;
 +              } catch (e) {
 +                      return false;
 +              }
 +      } else {
 +              return false;
 +      }
 +    }-*/;
 +
 +    @Override
 +    public void onBrowserEvent(Event event) {
 +        super.onBrowserEvent(event);
 +        if (event.getTypeInt() == Event.ONLOAD) {
 +            Util.notifyParentOfSizeChange(this, true);
 +            event.cancelBubble(true);
 +        }
 +    }
 +
 +}
index 5cbfabbb11267604013d28d33091714d8c52c7d6,0000000000000000000000000000000000000000..d09b81e1e1bde8d23ef028b6062dc0a487ff5af5
mode 100644,000000..100644
--- /dev/null
@@@ -1,593 -1,0 +1,593 @@@
-         el.addEventListener("dragstart",  function(ev) {
 +/*
 +@VaadinApache2LicenseForJavaFiles@
 + */
 +package com.vaadin.terminal.gwt.client.ui.draganddropwrapper;
 +
 +import java.util.ArrayList;
 +import java.util.List;
 +import java.util.Map;
 +
 +import com.google.gwt.core.client.GWT;
 +import com.google.gwt.core.client.JsArrayString;
 +import com.google.gwt.core.client.Scheduler;
 +import com.google.gwt.dom.client.NativeEvent;
 +import com.google.gwt.event.dom.client.MouseDownEvent;
 +import com.google.gwt.event.dom.client.MouseDownHandler;
 +import com.google.gwt.event.dom.client.TouchStartEvent;
 +import com.google.gwt.event.dom.client.TouchStartHandler;
 +import com.google.gwt.user.client.Command;
 +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.Widget;
 +import com.google.gwt.xhr.client.ReadyStateChangeHandler;
 +import com.google.gwt.xhr.client.XMLHttpRequest;
 +import com.vaadin.terminal.gwt.client.ApplicationConnection;
 +import com.vaadin.terminal.gwt.client.ComponentConnector;
 +import com.vaadin.terminal.gwt.client.ConnectorMap;
 +import com.vaadin.terminal.gwt.client.LayoutManager;
 +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.VConsole;
 +import com.vaadin.terminal.gwt.client.VTooltip;
 +import com.vaadin.terminal.gwt.client.ValueMap;
 +import com.vaadin.terminal.gwt.client.ui.customcomponent.VCustomComponent;
 +import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
 +import com.vaadin.terminal.gwt.client.ui.dd.HorizontalDropLocation;
 +import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
 +import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
 +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
 +import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
 +import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler;
 +import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
 +import com.vaadin.terminal.gwt.client.ui.dd.VHtml5DragEvent;
 +import com.vaadin.terminal.gwt.client.ui.dd.VHtml5File;
 +import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
 +import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation;
 +
 +/**
 + * 
 + * Must have features pending:
 + * 
 + * drop details: locations + sizes in document hierarchy up to wrapper
 + * 
 + */
 +public class VDragAndDropWrapper extends VCustomComponent implements
 +        VHasDropHandler {
 +    public static final String DRAG_START_MODE = "dragStartMode";
 +    public static final String HTML5_DATA_FLAVORS = "html5-data-flavors";
 +
 +    private static final String CLASSNAME = "v-ddwrapper";
 +    protected static final String DRAGGABLE = "draggable";
 +
 +    public VDragAndDropWrapper() {
 +        super();
 +        sinkEvents(VTooltip.TOOLTIP_EVENTS);
 +
 +        hookHtml5Events(getElement());
 +        setStyleName(CLASSNAME);
 +        addDomHandler(new MouseDownHandler() {
 +            public void onMouseDown(MouseDownEvent event) {
 +                if (startDrag(event.getNativeEvent())) {
 +                    event.preventDefault(); // prevent text selection
 +                }
 +            }
 +        }, MouseDownEvent.getType());
 +
 +        addDomHandler(new TouchStartHandler() {
 +            public void onTouchStart(TouchStartEvent event) {
 +                if (startDrag(event.getNativeEvent())) {
 +                    /*
 +                     * Dont let eg. panel start scrolling.
 +                     */
 +                    event.stopPropagation();
 +                }
 +            }
 +        }, TouchStartEvent.getType());
 +
 +        sinkEvents(Event.TOUCHEVENTS);
 +    }
 +
 +    @Override
 +    public void onBrowserEvent(Event event) {
 +        super.onBrowserEvent(event);
 +
 +        if (client != null) {
 +            client.handleTooltipEvent(event, this);
 +        }
 +    }
 +
 +    /**
 +     * Starts a drag and drop operation from mousedown or touchstart event if
 +     * required conditions are met.
 +     * 
 +     * @param event
 +     * @return true if the event was handled as a drag start event
 +     */
 +    private boolean startDrag(NativeEvent event) {
 +        if (dragStartMode == WRAPPER || dragStartMode == COMPONENT) {
 +            VTransferable transferable = new VTransferable();
 +            transferable.setDragSource(ConnectorMap.get(client).getConnector(
 +                    VDragAndDropWrapper.this));
 +
 +            ComponentConnector paintable = Util.findPaintable(client,
 +                    (Element) event.getEventTarget().cast());
 +            Widget widget = paintable.getWidget();
 +            transferable.setData("component", paintable);
 +            VDragEvent dragEvent = VDragAndDropManager.get().startDrag(
 +                    transferable, event, true);
 +
 +            transferable.setData("mouseDown", MouseEventDetailsBuilder
 +                    .buildMouseEventDetails(event).serialize());
 +
 +            if (dragStartMode == WRAPPER) {
 +                dragEvent.createDragImage(getElement(), true);
 +            } else {
 +                dragEvent.createDragImage(widget.getElement(), true);
 +            }
 +            return true;
 +        }
 +        return false;
 +    }
 +
 +    protected final static int NONE = 0;
 +    protected final static int COMPONENT = 1;
 +    protected final static int WRAPPER = 2;
 +    protected final static int HTML5 = 3;
 +
 +    protected int dragStartMode;
 +
 +    ApplicationConnection client;
 +    VAbstractDropHandler dropHandler;
 +    private VDragEvent vaadinDragEvent;
 +
 +    int filecounter = 0;
 +    Map<String, String> fileIdToReceiver;
 +    ValueMap html5DataFlavors;
 +    private Element dragStartElement;
 +
 +    protected void initDragStartMode() {
 +        Element div = getElement();
 +        if (dragStartMode == HTML5) {
 +            if (dragStartElement == null) {
 +                dragStartElement = getDragStartElement();
 +                dragStartElement.setPropertyBoolean(DRAGGABLE, true);
 +                VConsole.log("draggable = "
 +                        + dragStartElement.getPropertyBoolean(DRAGGABLE));
 +                hookHtml5DragStart(dragStartElement);
 +                VConsole.log("drag start listeners hooked.");
 +            }
 +        } else {
 +            dragStartElement = null;
 +            if (div.hasAttribute(DRAGGABLE)) {
 +                div.removeAttribute(DRAGGABLE);
 +            }
 +        }
 +    }
 +
 +    protected Element getDragStartElement() {
 +        return getElement();
 +    }
 +
 +    private boolean uploading;
 +
 +    private ReadyStateChangeHandler readyStateChangeHandler = new ReadyStateChangeHandler() {
 +        public void onReadyStateChange(XMLHttpRequest xhr) {
 +            if (xhr.getReadyState() == XMLHttpRequest.DONE) {
 +                // visit server for possible
 +                // variable changes
 +                client.sendPendingVariableChanges();
 +                uploading = false;
 +                startNextUpload();
 +                xhr.clearOnReadyStateChange();
 +            }
 +        }
 +    };
 +    private Timer dragleavetimer;
 +
 +    void startNextUpload() {
 +        Scheduler.get().scheduleDeferred(new Command() {
 +
 +            public void execute() {
 +                if (!uploading) {
 +                    if (fileIds.size() > 0) {
 +
 +                        uploading = true;
 +                        final Integer fileId = fileIds.remove(0);
 +                        VHtml5File file = files.remove(0);
 +                        final String receiverUrl = client
 +                                .translateVaadinUri(fileIdToReceiver
 +                                        .remove(fileId.toString()));
 +                        ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR
 +                                .create();
 +                        extendedXHR
 +                                .setOnReadyStateChange(readyStateChangeHandler);
 +                        extendedXHR.open("POST", receiverUrl);
 +                        extendedXHR.postFile(file);
 +                    }
 +                }
 +
 +            }
 +        });
 +
 +    }
 +
 +    public boolean html5DragStart(VHtml5DragEvent event) {
 +        if (dragStartMode == HTML5) {
 +            /*
 +             * Populate html5 payload with dataflavors from the serverside
 +             */
 +            JsArrayString flavors = html5DataFlavors.getKeyArray();
 +            for (int i = 0; i < flavors.length(); i++) {
 +                String flavor = flavors.get(i);
 +                event.setHtml5DataFlavor(flavor,
 +                        html5DataFlavors.getString(flavor));
 +            }
 +            event.setEffectAllowed("copy");
 +            return true;
 +        }
 +        return false;
 +    }
 +
 +    public boolean html5DragEnter(VHtml5DragEvent event) {
 +        if (dropHandler == null) {
 +            return true;
 +        }
 +        try {
 +            if (dragleavetimer != null) {
 +                // returned quickly back to wrapper
 +                dragleavetimer.cancel();
 +                dragleavetimer = null;
 +            }
 +            if (VDragAndDropManager.get().getCurrentDropHandler() != getDropHandler()) {
 +                VTransferable transferable = new VTransferable();
 +                transferable.setDragSource(ConnectorMap.get(client)
 +                        .getConnector(this));
 +
 +                vaadinDragEvent = VDragAndDropManager.get().startDrag(
 +                        transferable, event, false);
 +                VDragAndDropManager.get().setCurrentDropHandler(
 +                        getDropHandler());
 +            }
 +            try {
 +                event.preventDefault();
 +                event.stopPropagation();
 +            } catch (Exception e) {
 +                // VConsole.log("IE9 fails");
 +            }
 +            return false;
 +        } catch (Exception e) {
 +            GWT.getUncaughtExceptionHandler().onUncaughtException(e);
 +            return true;
 +        }
 +    }
 +
 +    public boolean html5DragLeave(VHtml5DragEvent event) {
 +        if (dropHandler == null) {
 +            return true;
 +        }
 +
 +        try {
 +            dragleavetimer = new Timer() {
 +                @Override
 +                public void run() {
 +                    // Yes, dragleave happens before drop. Makes no sense to me.
 +                    // IMO shouldn't fire leave at all if drop happens (I guess
 +                    // this
 +                    // is what IE does).
 +                    // In Vaadin we fire it only if drop did not happen.
 +                    if (vaadinDragEvent != null
 +                            && VDragAndDropManager.get()
 +                                    .getCurrentDropHandler() == getDropHandler()) {
 +                        VDragAndDropManager.get().interruptDrag();
 +                    }
 +                }
 +            };
 +            dragleavetimer.schedule(350);
 +            try {
 +                event.preventDefault();
 +                event.stopPropagation();
 +            } catch (Exception e) {
 +                // VConsole.log("IE9 fails");
 +            }
 +            return false;
 +        } catch (Exception e) {
 +            GWT.getUncaughtExceptionHandler().onUncaughtException(e);
 +            return true;
 +        }
 +    }
 +
 +    public boolean html5DragOver(VHtml5DragEvent event) {
 +        if (dropHandler == null) {
 +            return true;
 +        }
 +
 +        if (dragleavetimer != null) {
 +            // returned quickly back to wrapper
 +            dragleavetimer.cancel();
 +            dragleavetimer = null;
 +        }
 +
 +        vaadinDragEvent.setCurrentGwtEvent(event);
 +        getDropHandler().dragOver(vaadinDragEvent);
 +
 +        String s = event.getEffectAllowed();
 +        if ("all".equals(s) || s.contains("opy")) {
 +            event.setDropEffect("copy");
 +        } else {
 +            event.setDropEffect(s);
 +        }
 +
 +        try {
 +            event.preventDefault();
 +            event.stopPropagation();
 +        } catch (Exception e) {
 +            // VConsole.log("IE9 fails");
 +        }
 +        return false;
 +    }
 +
 +    public boolean html5DragDrop(VHtml5DragEvent event) {
 +        if (dropHandler == null || !currentlyValid) {
 +            return true;
 +        }
 +        try {
 +
 +            VTransferable transferable = vaadinDragEvent.getTransferable();
 +
 +            JsArrayString types = event.getTypes();
 +            for (int i = 0; i < types.length(); i++) {
 +                String type = types.get(i);
 +                if (isAcceptedType(type)) {
 +                    String data = event.getDataAsText(type);
 +                    if (data != null) {
 +                        transferable.setData(type, data);
 +                    }
 +                }
 +            }
 +
 +            int fileCount = event.getFileCount();
 +            if (fileCount > 0) {
 +                transferable.setData("filecount", fileCount);
 +                for (int i = 0; i < fileCount; i++) {
 +                    final int fileId = filecounter++;
 +                    final VHtml5File file = event.getFile(i);
 +                    transferable.setData("fi" + i, "" + fileId);
 +                    transferable.setData("fn" + i, file.getName());
 +                    transferable.setData("ft" + i, file.getType());
 +                    transferable.setData("fs" + i, file.getSize());
 +                    queueFilePost(fileId, file);
 +                }
 +
 +            }
 +
 +            VDragAndDropManager.get().endDrag();
 +            vaadinDragEvent = null;
 +            try {
 +                event.preventDefault();
 +                event.stopPropagation();
 +            } catch (Exception e) {
 +                // VConsole.log("IE9 fails");
 +            }
 +            return false;
 +        } catch (Exception e) {
 +            GWT.getUncaughtExceptionHandler().onUncaughtException(e);
 +            return true;
 +        }
 +
 +    }
 +
 +    protected String[] acceptedTypes = new String[] { "Text", "Url",
 +            "text/html", "text/plain", "text/rtf" };
 +
 +    private boolean isAcceptedType(String type) {
 +        for (String t : acceptedTypes) {
 +            if (t.equals(type)) {
 +                return true;
 +            }
 +        }
 +        return false;
 +    }
 +
 +    static class ExtendedXHR extends XMLHttpRequest {
 +
 +        protected ExtendedXHR() {
 +        }
 +
 +        public final native void postFile(VHtml5File file)
 +        /*-{
 +
 +            this.setRequestHeader('Content-Type', 'multipart/form-data');
 +            this.send(file);
 +        }-*/;
 +
 +    }
 +
 +    /**
 +     * Currently supports only FF36 as no other browser supports natively File
 +     * api.
 +     * 
 +     * @param fileId
 +     * @param data
 +     */
 +    List<Integer> fileIds = new ArrayList<Integer>();
 +    List<VHtml5File> files = new ArrayList<VHtml5File>();
 +
 +    private void queueFilePost(final int fileId, final VHtml5File file) {
 +        fileIds.add(fileId);
 +        files.add(file);
 +    }
 +
 +    public VDropHandler getDropHandler() {
 +        return dropHandler;
 +    }
 +
 +    protected VerticalDropLocation verticalDropLocation;
 +    protected HorizontalDropLocation horizontalDropLocation;
 +    private VerticalDropLocation emphasizedVDrop;
 +    private HorizontalDropLocation emphasizedHDrop;
 +
 +    /**
 +     * Flag used by html5 dd
 +     */
 +    private boolean currentlyValid;
 +
 +    private static final String OVER_STYLE = "v-ddwrapper-over";
 +
 +    public class CustomDropHandler extends VAbstractDropHandler {
 +
 +        @Override
 +        public void dragEnter(VDragEvent drag) {
 +            updateDropDetails(drag);
 +            currentlyValid = false;
 +            super.dragEnter(drag);
 +        }
 +
 +        @Override
 +        public void dragLeave(VDragEvent drag) {
 +            deEmphasis(true);
 +            dragleavetimer = null;
 +        }
 +
 +        @Override
 +        public void dragOver(final VDragEvent drag) {
 +            boolean detailsChanged = updateDropDetails(drag);
 +            if (detailsChanged) {
 +                currentlyValid = false;
 +                validate(new VAcceptCallback() {
 +                    public void accepted(VDragEvent event) {
 +                        dragAccepted(drag);
 +                    }
 +                }, drag);
 +            }
 +        }
 +
 +        @Override
 +        public boolean drop(VDragEvent drag) {
 +            deEmphasis(true);
 +
 +            Map<String, Object> dd = drag.getDropDetails();
 +
 +            // this is absolute layout based, and we may want to set
 +            // component
 +            // relatively to where the drag ended.
 +            // need to add current location of the drop area
 +
 +            int absoluteLeft = getAbsoluteLeft();
 +            int absoluteTop = getAbsoluteTop();
 +
 +            dd.put("absoluteLeft", absoluteLeft);
 +            dd.put("absoluteTop", absoluteTop);
 +
 +            if (verticalDropLocation != null) {
 +                dd.put("verticalLocation", verticalDropLocation.toString());
 +                dd.put("horizontalLocation", horizontalDropLocation.toString());
 +            }
 +
 +            return super.drop(drag);
 +        }
 +
 +        @Override
 +        protected void dragAccepted(VDragEvent drag) {
 +            currentlyValid = true;
 +            emphasis(drag);
 +        }
 +
 +        @Override
 +        public ComponentConnector getConnector() {
 +            return ConnectorMap.get(client).getConnector(
 +                    VDragAndDropWrapper.this);
 +        }
 +
 +        public ApplicationConnection getApplicationConnection() {
 +            return client;
 +        }
 +
 +    }
 +
 +    protected native void hookHtml5DragStart(Element el)
 +    /*-{
 +        var me = this;
-         }, false);
++        el.addEventListener("dragstart",  $entry(function(ev) {
 +            return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
-             el.addEventListener("dragenter",  function(ev) {
++        }), false);
 +    }-*/;
 +
 +    /**
 +     * Prototype code, memory leak risk.
 +     * 
 +     * @param el
 +     */
 +    protected native void hookHtml5Events(Element el)
 +    /*-{
 +            var me = this;
 +
-             }, false);
++            el.addEventListener("dragenter",  $entry(function(ev) {
 +                return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
-             el.addEventListener("dragleave",  function(ev) {
++            }), false);
 +
-             }, false);
++            el.addEventListener("dragleave",  $entry(function(ev) {
 +                return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
-             el.addEventListener("dragover",  function(ev) {
++            }), false);
 +
-             }, false);
++            el.addEventListener("dragover",  $entry(function(ev) {
 +                return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
-             el.addEventListener("drop",  function(ev) {
++            }), false);
 +
-             }, false);
++            el.addEventListener("drop",  $entry(function(ev) {
 +                return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
++            }), false);
 +    }-*/;
 +
 +    public boolean updateDropDetails(VDragEvent drag) {
 +        VerticalDropLocation oldVL = verticalDropLocation;
 +        verticalDropLocation = DDUtil.getVerticalDropLocation(getElement(),
 +                drag.getCurrentGwtEvent(), 0.2);
 +        drag.getDropDetails().put("verticalLocation",
 +                verticalDropLocation.toString());
 +        HorizontalDropLocation oldHL = horizontalDropLocation;
 +        horizontalDropLocation = DDUtil.getHorizontalDropLocation(getElement(),
 +                drag.getCurrentGwtEvent(), 0.2);
 +        drag.getDropDetails().put("horizontalLocation",
 +                horizontalDropLocation.toString());
 +        if (oldHL != horizontalDropLocation || oldVL != verticalDropLocation) {
 +            return true;
 +        } else {
 +            return false;
 +        }
 +    }
 +
 +    protected void deEmphasis(boolean doLayout) {
 +        if (emphasizedVDrop != null) {
 +            VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, false);
 +            VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
 +                    + emphasizedVDrop.toString().toLowerCase(), false);
 +            VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
 +                    + emphasizedHDrop.toString().toLowerCase(), false);
 +        }
 +        if (doLayout) {
 +            notifySizePotentiallyChanged();
 +        }
 +    }
 +
 +    private void notifySizePotentiallyChanged() {
 +        LayoutManager.get(client).setNeedsMeasure(
 +                ConnectorMap.get(client).getConnector(getElement()));
 +    }
 +
 +    protected void emphasis(VDragEvent drag) {
 +        deEmphasis(false);
 +        VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, true);
 +        VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
 +                + verticalDropLocation.toString().toLowerCase(), true);
 +        VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
 +                + horizontalDropLocation.toString().toLowerCase(), true);
 +        emphasizedVDrop = verticalDropLocation;
 +        emphasizedHDrop = horizontalDropLocation;
 +
 +        // TODO build (to be an example) an emphasis mode where drag image
 +        // is fitted before or after the content
 +        notifySizePotentiallyChanged();
 +    }
 +
 +}
index f819b0559adaef23d05a6151458e7db3efe47962,0000000000000000000000000000000000000000..bb511524e57b69c1a918505506bc4a569abfe0a7
mode 100644,000000..100644
--- /dev/null
@@@ -1,69 -1,0 +1,69 @@@
-         el.attachEvent("ondragstart",  function(ev) {
 +/*
 +@VaadinApache2LicenseForJavaFiles@
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.draganddropwrapper;
 +
 +import com.google.gwt.dom.client.AnchorElement;
 +import com.google.gwt.dom.client.Document;
 +import com.google.gwt.user.client.Element;
 +import com.vaadin.terminal.gwt.client.VConsole;
 +
 +public class VDragAndDropWrapperIE extends VDragAndDropWrapper {
 +    private AnchorElement anchor = null;
 +
 +    @Override
 +    protected Element getDragStartElement() {
 +        VConsole.log("IE get drag start element...");
 +        Element div = getElement();
 +        if (dragStartMode == HTML5) {
 +            if (anchor == null) {
 +                anchor = Document.get().createAnchorElement();
 +                anchor.setHref("#");
 +                anchor.setClassName("drag-start");
 +                div.appendChild(anchor);
 +            }
 +            VConsole.log("IE get drag start element...");
 +            return (Element) anchor.cast();
 +        } else {
 +            if (anchor != null) {
 +                div.removeChild(anchor);
 +                anchor = null;
 +            }
 +            return div;
 +        }
 +    }
 +
 +    @Override
 +    protected native void hookHtml5DragStart(Element el)
 +    /*-{
 +        var me = this;
 +
-         });
++        el.attachEvent("ondragstart",  $entry(function(ev) {
 +            return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
-         el.attachEvent("ondragenter",  function(ev) {
++        }));
 +    }-*/;
 +
 +    @Override
 +    protected native void hookHtml5Events(Element el)
 +    /*-{
 +        var me = this;
 +
-         });
++        el.attachEvent("ondragenter",  $entry(function(ev) {
 +            return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
-         el.attachEvent("ondragleave",  function(ev) {
++        }));
 +
-         });
++        el.attachEvent("ondragleave",  $entry(function(ev) {
 +            return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
-         el.attachEvent("ondragover",  function(ev) {
++        }));
 +
-         });
++        el.attachEvent("ondragover",  $entry(function(ev) {
 +            return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
-         el.attachEvent("ondrop",  function(ev) {
++        }));
 +
-         });
++        el.attachEvent("ondrop",  $entry(function(ev) {
 +            return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
++        }));
 +    }-*/;
 +
 +}
index 563ca04abe70e9454a7377e48d8b5d502a05799f,0000000000000000000000000000000000000000..c45c26c4ac9640e93dcc8c4fb95a32433b2cb944
mode 100644,000000..100644
--- /dev/null
@@@ -1,6699 -1,0 +1,6716 @@@
-             private static final int TOUCHSCROLL_TIMEOUT = 70;
 +/*
 +@VaadinApache2LicenseForJavaFiles@
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.table;
 +
 +import java.util.ArrayList;
 +import java.util.Collection;
 +import java.util.HashMap;
 +import java.util.HashSet;
 +import java.util.Iterator;
 +import java.util.LinkedList;
 +import java.util.List;
 +import java.util.Set;
 +
 +import com.google.gwt.core.client.JavaScriptObject;
 +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.NativeEvent;
++import com.google.gwt.dom.client.Node;
 +import com.google.gwt.dom.client.NodeList;
 +import com.google.gwt.dom.client.Style;
 +import com.google.gwt.dom.client.Style.Display;
 +import com.google.gwt.dom.client.Style.Position;
 +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.dom.client.TableRowElement;
 +import com.google.gwt.dom.client.TableSectionElement;
 +import com.google.gwt.dom.client.Touch;
 +import com.google.gwt.event.dom.client.BlurEvent;
 +import com.google.gwt.event.dom.client.BlurHandler;
 +import com.google.gwt.event.dom.client.ContextMenuEvent;
 +import com.google.gwt.event.dom.client.ContextMenuHandler;
 +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.event.dom.client.KeyPressEvent;
 +import com.google.gwt.event.dom.client.KeyPressHandler;
 +import com.google.gwt.event.dom.client.KeyUpEvent;
 +import com.google.gwt.event.dom.client.KeyUpHandler;
 +import com.google.gwt.event.dom.client.ScrollEvent;
 +import com.google.gwt.event.dom.client.ScrollHandler;
 +import com.google.gwt.event.dom.client.TouchStartEvent;
 +import com.google.gwt.event.dom.client.TouchStartHandler;
 +import com.google.gwt.event.logical.shared.CloseEvent;
 +import com.google.gwt.event.logical.shared.CloseHandler;
 +import com.google.gwt.user.client.Command;
 +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.Window;
 +import com.google.gwt.user.client.ui.FlowPanel;
 +import com.google.gwt.user.client.ui.HasWidgets;
 +import com.google.gwt.user.client.ui.Panel;
 +import com.google.gwt.user.client.ui.PopupPanel;
 +import com.google.gwt.user.client.ui.RootPanel;
 +import com.google.gwt.user.client.ui.UIObject;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.terminal.gwt.client.ApplicationConnection;
 +import com.vaadin.terminal.gwt.client.BrowserInfo;
 +import com.vaadin.terminal.gwt.client.ComponentConnector;
 +import com.vaadin.terminal.gwt.client.ComponentState;
 +import com.vaadin.terminal.gwt.client.ConnectorMap;
 +import com.vaadin.terminal.gwt.client.Focusable;
 +import com.vaadin.terminal.gwt.client.MouseEventDetails;
 +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
 +import com.vaadin.terminal.gwt.client.TooltipInfo;
 +import com.vaadin.terminal.gwt.client.UIDL;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.VConsole;
 +import com.vaadin.terminal.gwt.client.VTooltip;
 +import com.vaadin.terminal.gwt.client.ui.Action;
 +import com.vaadin.terminal.gwt.client.ui.ActionOwner;
 +import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel;
 +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
 +import com.vaadin.terminal.gwt.client.ui.TreeAction;
 +import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
 +import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
 +import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
 +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
 +import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
 +import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
 +import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
 +import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation;
 +import com.vaadin.terminal.gwt.client.ui.embedded.VEmbedded;
 +import com.vaadin.terminal.gwt.client.ui.label.VLabel;
 +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow;
 +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
 +
 +/**
 + * VScrollTable
 + * 
 + * VScrollTable is a FlowPanel having two widgets in it: * TableHead component *
 + * ScrollPanel
 + * 
 + * TableHead contains table's header and widgets + logic for resizing,
 + * reordering and hiding columns.
 + * 
 + * ScrollPanel contains VScrollTableBody object which handles content. To save
 + * some bandwidth and to improve clients responsiveness with loads of data, in
 + * VScrollTableBody all rows are not necessary rendered. There are "spacers" in
 + * VScrollTableBody to use the exact same space as non-rendered rows would use.
 + * This way we can use seamlessly traditional scrollbars and scrolling to fetch
 + * more rows instead of "paging".
 + * 
 + * In VScrollTable we listen to scroll events. On horizontal scrolling we also
 + * update TableHeads scroll position which has its scrollbars hidden. On
 + * vertical scroll events we will check if we are reaching the end of area where
 + * we have rows rendered and
 + * 
 + * TODO implement unregistering for child components in Cells
 + */
 +public class VScrollTable extends FlowPanel implements HasWidgets,
 +        ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable,
 +        ActionOwner {
 +
 +    public enum SelectMode {
 +        NONE(0), SINGLE(1), MULTI(2);
 +        private int id;
 +
 +        private SelectMode(int id) {
 +            this.id = id;
 +        }
 +
 +        public int getId() {
 +            return id;
 +        }
 +    }
 +
 +    private static final String ROW_HEADER_COLUMN_KEY = "0";
 +
 +    public static final String CLASSNAME = "v-table";
 +    public static final String CLASSNAME_SELECTION_FOCUS = CLASSNAME + "-focus";
 +
 +    public static final String ATTRIBUTE_PAGEBUFFER_FIRST = "pb-ft";
 +    public static final String ATTRIBUTE_PAGEBUFFER_LAST = "pb-l";
 +
 +    public static final String ITEM_CLICK_EVENT_ID = "itemClick";
 +    public static final String HEADER_CLICK_EVENT_ID = "handleHeaderClick";
 +    public static final String FOOTER_CLICK_EVENT_ID = "handleFooterClick";
 +    public static final String COLUMN_RESIZE_EVENT_ID = "columnResize";
 +    public static final String COLUMN_REORDER_EVENT_ID = "columnReorder";
 +
 +    private static final double CACHE_RATE_DEFAULT = 2;
 +
 +    /**
 +     * The default multi select mode where simple left clicks only selects one
 +     * item, CTRL+left click selects multiple items and SHIFT-left click selects
 +     * a range of items.
 +     */
 +    private static final int MULTISELECT_MODE_DEFAULT = 0;
 +
 +    /**
 +     * The simple multiselect mode is what the table used to have before
 +     * ctrl/shift selections were added. That is that when this is set clicking
 +     * on an item selects/deselects the item and no ctrl/shift selections are
 +     * available.
 +     */
 +    private static final int MULTISELECT_MODE_SIMPLE = 1;
 +
 +    /**
 +     * multiple of pagelength which component will cache when requesting more
 +     * rows
 +     */
 +    private double cache_rate = CACHE_RATE_DEFAULT;
 +    /**
 +     * fraction of pageLenght which can be scrolled without making new request
 +     */
 +    private double cache_react_rate = 0.75 * cache_rate;
 +
 +    public static final char ALIGN_CENTER = 'c';
 +    public static final char ALIGN_LEFT = 'b';
 +    public static final char ALIGN_RIGHT = 'e';
 +    private static final int CHARCODE_SPACE = 32;
 +    private int firstRowInViewPort = 0;
 +    private int pageLength = 15;
 +    private int lastRequestedFirstvisible = 0; // to detect "serverside scroll"
 +
 +    protected boolean showRowHeaders = false;
 +
 +    private String[] columnOrder;
 +
 +    protected ApplicationConnection client;
 +    protected String paintableId;
 +
 +    boolean immediate;
 +    private boolean nullSelectionAllowed = true;
 +
 +    private SelectMode selectMode = SelectMode.NONE;
 +
 +    private final HashSet<String> selectedRowKeys = new HashSet<String>();
 +
 +    /*
 +     * When scrolling and selecting at the same time, the selections are not in
 +     * sync with the server while retrieving new rows (until key is released).
 +     */
 +    private HashSet<Object> unSyncedselectionsBeforeRowFetch;
 +
 +    /*
 +     * These are used when jumping between pages when pressing Home and End
 +     */
 +    boolean selectLastItemInNextRender = false;
 +    boolean selectFirstItemInNextRender = false;
 +    boolean focusFirstItemInNextRender = false;
 +    boolean focusLastItemInNextRender = false;
 +
 +    /*
 +     * The currently focused row
 +     */
 +    VScrollTableRow focusedRow;
 +
 +    /*
 +     * Helper to store selection range start in when using the keyboard
 +     */
 +    private VScrollTableRow selectionRangeStart;
 +
 +    /*
 +     * Flag for notifying when the selection has changed and should be sent to
 +     * the server
 +     */
 +    boolean selectionChanged = false;
 +
 +    /*
 +     * The speed (in pixels) which the scrolling scrolls vertically/horizontally
 +     */
 +    private int scrollingVelocity = 10;
 +
 +    private Timer scrollingVelocityTimer = null;
 +
 +    String[] bodyActionKeys;
 +
 +    private boolean enableDebug = false;
 +
 +    /**
 +     * Represents a select range of rows
 +     */
 +    private class SelectionRange {
 +        private VScrollTableRow startRow;
 +        private final int length;
 +
 +        /**
 +         * Constuctor.
 +         */
 +        public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) {
 +            VScrollTableRow endRow;
 +            if (row2.isBefore(row1)) {
 +                startRow = row2;
 +                endRow = row1;
 +            } else {
 +                startRow = row1;
 +                endRow = row2;
 +            }
 +            length = endRow.getIndex() - startRow.getIndex() + 1;
 +        }
 +
 +        public SelectionRange(VScrollTableRow row, int length) {
 +            startRow = row;
 +            this.length = length;
 +        }
 +
 +        /*
 +         * (non-Javadoc)
 +         * 
 +         * @see java.lang.Object#toString()
 +         */
 +        @Override
 +        public String toString() {
 +            return startRow.getKey() + "-" + length;
 +        }
 +
 +        private boolean inRange(VScrollTableRow row) {
 +            return row.getIndex() >= startRow.getIndex()
 +                    && row.getIndex() < startRow.getIndex() + length;
 +        }
 +
 +        public Collection<SelectionRange> split(VScrollTableRow row) {
 +            assert row.isAttached();
 +            ArrayList<SelectionRange> ranges = new ArrayList<SelectionRange>(2);
 +
 +            int endOfFirstRange = row.getIndex() - 1;
 +            if (!(endOfFirstRange - startRow.getIndex() < 0)) {
 +                // create range of first part unless its length is < 1
 +                ranges.add(new SelectionRange(startRow, endOfFirstRange
 +                        - startRow.getIndex() + 1));
 +            }
 +            int startOfSecondRange = row.getIndex() + 1;
 +            if (!(getEndIndex() - startOfSecondRange < 0)) {
 +                // create range of second part unless its length is < 1
 +                VScrollTableRow startOfRange = scrollBody
 +                        .getRowByRowIndex(startOfSecondRange);
 +                ranges.add(new SelectionRange(startOfRange, getEndIndex()
 +                        - startOfSecondRange + 1));
 +            }
 +            return ranges;
 +        }
 +
 +        private int getEndIndex() {
 +            return startRow.getIndex() + length - 1;
 +        }
 +
 +    };
 +
 +    private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>();
 +
 +    boolean initializedAndAttached = false;
 +
 +    /**
 +     * Flag to indicate if a column width recalculation is needed due update.
 +     */
 +    boolean headerChangedDuringUpdate = false;
 +
 +    protected final TableHead tHead = new TableHead();
 +
 +    final TableFooter tFoot = new TableFooter();
 +
 +    final FocusableScrollPanel scrollBodyPanel = new FocusableScrollPanel(true);
 +
 +    private KeyPressHandler navKeyPressHandler = new KeyPressHandler() {
 +        public void onKeyPress(KeyPressEvent keyPressEvent) {
 +            // This is used for Firefox only, since Firefox auto-repeat
 +            // works correctly only if we use a key press handler, other
 +            // browsers handle it correctly when using a key down handler
 +            if (!BrowserInfo.get().isGecko()) {
 +                return;
 +            }
 +
 +            NativeEvent event = keyPressEvent.getNativeEvent();
 +            if (!enabled) {
 +                // Cancel default keyboard events on a disabled Table
 +                // (prevents scrolling)
 +                event.preventDefault();
 +            } else if (hasFocus) {
 +                // Key code in Firefox/onKeyPress is present only for
 +                // special keys, otherwise 0 is returned
 +                int keyCode = event.getKeyCode();
 +                if (keyCode == 0 && event.getCharCode() == ' ') {
 +                    // Provide a keyCode for space to be compatible with
 +                    // FireFox keypress event
 +                    keyCode = CHARCODE_SPACE;
 +                }
 +
 +                if (handleNavigation(keyCode,
 +                        event.getCtrlKey() || event.getMetaKey(),
 +                        event.getShiftKey())) {
 +                    event.preventDefault();
 +                }
 +
 +                startScrollingVelocityTimer();
 +            }
 +        }
 +
 +    };
 +
 +    private KeyUpHandler navKeyUpHandler = new KeyUpHandler() {
 +
 +        public void onKeyUp(KeyUpEvent keyUpEvent) {
 +            NativeEvent event = keyUpEvent.getNativeEvent();
 +            int keyCode = event.getKeyCode();
 +
 +            if (!isFocusable()) {
 +                cancelScrollingVelocityTimer();
 +            } else if (isNavigationKey(keyCode)) {
 +                if (keyCode == getNavigationDownKey()
 +                        || keyCode == getNavigationUpKey()) {
 +                    /*
 +                     * in multiselect mode the server may still have value from
 +                     * previous page. Clear it unless doing multiselection or
 +                     * just moving focus.
 +                     */
 +                    if (!event.getShiftKey() && !event.getCtrlKey()) {
 +                        instructServerToForgetPreviousSelections();
 +                    }
 +                    sendSelectedRows();
 +                }
 +                cancelScrollingVelocityTimer();
 +                navKeyDown = false;
 +            }
 +        }
 +    };
 +
 +    private KeyDownHandler navKeyDownHandler = new KeyDownHandler() {
 +
 +        public void onKeyDown(KeyDownEvent keyDownEvent) {
 +            NativeEvent event = keyDownEvent.getNativeEvent();
 +            // This is not used for Firefox
 +            if (BrowserInfo.get().isGecko()) {
 +                return;
 +            }
 +
 +            if (!enabled) {
 +                // Cancel default keyboard events on a disabled Table
 +                // (prevents scrolling)
 +                event.preventDefault();
 +            } else if (hasFocus) {
 +                if (handleNavigation(event.getKeyCode(), event.getCtrlKey()
 +                        || event.getMetaKey(), event.getShiftKey())) {
 +                    navKeyDown = true;
 +                    event.preventDefault();
 +                }
 +
 +                startScrollingVelocityTimer();
 +            }
 +        }
 +    };
 +    int totalRows;
 +
 +    private Set<String> collapsedColumns;
 +
 +    final RowRequestHandler rowRequestHandler;
 +    VScrollTableBody scrollBody;
 +    private int firstvisible = 0;
 +    private boolean sortAscending;
 +    private String sortColumn;
 +    private String oldSortColumn;
 +    private boolean columnReordering;
 +
 +    /**
 +     * This map contains captions and icon urls for actions like: * "33_c" ->
 +     * "Edit" * "33_i" -> "http://dom.com/edit.png"
 +     */
 +    private final HashMap<Object, String> actionMap = new HashMap<Object, String>();
 +    private String[] visibleColOrder;
 +    private boolean initialContentReceived = false;
 +    private Element scrollPositionElement;
 +    boolean enabled;
 +    boolean showColHeaders;
 +    boolean showColFooters;
 +
 +    /** flag to indicate that table body has changed */
 +    private boolean isNewBody = true;
 +
 +    /*
 +     * Read from the "recalcWidths" -attribute. When it is true, the table will
 +     * recalculate the widths for columns - desirable in some cases. For #1983,
 +     * marked experimental.
 +     */
 +    boolean recalcWidths = false;
 +
 +    boolean rendering = false;
 +    private boolean hasFocus = false;
 +    private int dragmode;
 +
 +    private int multiselectmode;
 +    int tabIndex;
 +    private TouchScrollDelegate touchScrollDelegate;
 +
 +    int lastRenderedHeight;
 +
 +    /**
 +     * Values (serverCacheFirst+serverCacheLast) sent by server that tells which
 +     * rows (indexes) are in the server side cache (page buffer). -1 means
 +     * unknown. The server side cache row MUST MATCH the client side cache rows.
 +     * 
 +     * If the client side cache contains additional rows with e.g. buttons, it
 +     * will cause out of sync when such a button is pressed.
 +     * 
 +     * If the server side cache contains additional rows with e.g. buttons,
 +     * scrolling in the client will cause empty buttons to be rendered
 +     * (cached=true request for non-existing components)
 +     */
 +    int serverCacheFirst = -1;
 +    int serverCacheLast = -1;
 +
 +    boolean sizeNeedsInit = true;
 +
 +    /**
 +     * Used to recall the position of an open context menu if we need to close
 +     * and reopen it during a row update.
 +     */
 +    class ContextMenuDetails {
 +        String rowKey;
 +        int left;
 +        int top;
 +
 +        ContextMenuDetails(String rowKey, int left, int top) {
 +            this.rowKey = rowKey;
 +            this.left = left;
 +            this.top = top;
 +        }
 +    }
 +
 +    protected ContextMenuDetails contextMenu = null;
 +
 +    public VScrollTable() {
 +        setMultiSelectMode(MULTISELECT_MODE_DEFAULT);
 +
 +        scrollBodyPanel.setStyleName(CLASSNAME + "-body-wrapper");
 +        scrollBodyPanel.addFocusHandler(this);
 +        scrollBodyPanel.addBlurHandler(this);
 +
 +        scrollBodyPanel.addScrollHandler(this);
 +        scrollBodyPanel.setStyleName(CLASSNAME + "-body");
 +
 +        /*
 +         * Firefox auto-repeat works correctly only if we use a key press
 +         * handler, other browsers handle it correctly when using a key down
 +         * handler
 +         */
 +        if (BrowserInfo.get().isGecko()) {
 +            scrollBodyPanel.addKeyPressHandler(navKeyPressHandler);
 +        } else {
 +            scrollBodyPanel.addKeyDownHandler(navKeyDownHandler);
 +        }
 +        scrollBodyPanel.addKeyUpHandler(navKeyUpHandler);
 +
 +        scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS);
 +        scrollBodyPanel.addDomHandler(new TouchStartHandler() {
 +            public void onTouchStart(TouchStartEvent event) {
 +                getTouchScrollDelegate().onTouchStart(event);
 +            }
 +        }, TouchStartEvent.getType());
 +
 +        scrollBodyPanel.sinkEvents(Event.ONCONTEXTMENU);
 +        scrollBodyPanel.addDomHandler(new ContextMenuHandler() {
 +            public void onContextMenu(ContextMenuEvent event) {
 +                handleBodyContextMenu(event);
 +            }
 +        }, ContextMenuEvent.getType());
 +
 +        setStyleName(CLASSNAME);
 +
 +        add(tHead);
 +        add(scrollBodyPanel);
 +        add(tFoot);
 +
 +        rowRequestHandler = new RowRequestHandler();
 +    }
 +
 +    public void init(ApplicationConnection client) {
 +        this.client = client;
 +        // Add a handler to clear saved context menu details when the menu
 +        // closes. See #8526.
 +        client.getContextMenu().addCloseHandler(new CloseHandler<PopupPanel>() {
 +            public void onClose(CloseEvent<PopupPanel> event) {
 +                contextMenu = null;
 +            }
 +        });
 +    }
 +
 +    protected TouchScrollDelegate getTouchScrollDelegate() {
 +        if (touchScrollDelegate == null) {
 +            touchScrollDelegate = new TouchScrollDelegate(
 +                    scrollBodyPanel.getElement());
++            touchScrollDelegate.setScrollHandler(this);
 +        }
 +        return touchScrollDelegate;
 +
 +    }
 +
 +    private void handleBodyContextMenu(ContextMenuEvent event) {
 +        if (enabled && bodyActionKeys != null) {
 +            int left = Util.getTouchOrMouseClientX(event.getNativeEvent());
 +            int top = Util.getTouchOrMouseClientY(event.getNativeEvent());
 +            top += Window.getScrollTop();
 +            left += Window.getScrollLeft();
 +            client.getContextMenu().showAt(this, left, top);
 +
 +            // Only prevent browser context menu if there are action handlers
 +            // registered
 +            event.stopPropagation();
 +            event.preventDefault();
 +        }
 +    }
 +
 +    /**
 +     * Fires a column resize event which sends the resize information to the
 +     * server.
 +     * 
 +     * @param columnId
 +     *            The columnId of the column which was resized
 +     * @param originalWidth
 +     *            The width in pixels of the column before the resize event
 +     * @param newWidth
 +     *            The width in pixels of the column after the resize event
 +     */
 +    private void fireColumnResizeEvent(String columnId, int originalWidth,
 +            int newWidth) {
 +        client.updateVariable(paintableId, "columnResizeEventColumn", columnId,
 +                false);
 +        client.updateVariable(paintableId, "columnResizeEventPrev",
 +                originalWidth, false);
 +        client.updateVariable(paintableId, "columnResizeEventCurr", newWidth,
 +                immediate);
 +
 +    }
 +
 +    /**
 +     * Non-immediate variable update of column widths for a collection of
 +     * columns.
 +     * 
 +     * @param columns
 +     *            the columns to trigger the events for.
 +     */
 +    private void sendColumnWidthUpdates(Collection<HeaderCell> columns) {
 +        String[] newSizes = new String[columns.size()];
 +        int ix = 0;
 +        for (HeaderCell cell : columns) {
 +            newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth();
 +        }
 +        client.updateVariable(paintableId, "columnWidthUpdates", newSizes,
 +                false);
 +    }
 +
 +    /**
 +     * Moves the focus one step down
 +     * 
 +     * @return Returns true if succeeded
 +     */
 +    private boolean moveFocusDown() {
 +        return moveFocusDown(0);
 +    }
 +
 +    /**
 +     * Moves the focus down by 1+offset rows
 +     * 
 +     * @return Returns true if succeeded, else false if the selection could not
 +     *         be move downwards
 +     */
 +    private boolean moveFocusDown(int offset) {
 +        if (isSelectable()) {
 +            if (focusedRow == null && scrollBody.iterator().hasNext()) {
 +                // FIXME should focus first visible from top, not first rendered
 +                // ??
 +                return setRowFocus((VScrollTableRow) scrollBody.iterator()
 +                        .next());
 +            } else {
 +                VScrollTableRow next = getNextRow(focusedRow, offset);
 +                if (next != null) {
 +                    return setRowFocus(next);
 +                }
 +            }
 +        }
 +
 +        return false;
 +    }
 +
 +    /**
 +     * Moves the selection one step up
 +     * 
 +     * @return Returns true if succeeded
 +     */
 +    private boolean moveFocusUp() {
 +        return moveFocusUp(0);
 +    }
 +
 +    /**
 +     * Moves the focus row upwards
 +     * 
 +     * @return Returns true if succeeded, else false if the selection could not
 +     *         be move upwards
 +     * 
 +     */
 +    private boolean moveFocusUp(int offset) {
 +        if (isSelectable()) {
 +            if (focusedRow == null && scrollBody.iterator().hasNext()) {
 +                // FIXME logic is exactly the same as in moveFocusDown, should
 +                // be the opposite??
 +                return setRowFocus((VScrollTableRow) scrollBody.iterator()
 +                        .next());
 +            } else {
 +                VScrollTableRow prev = getPreviousRow(focusedRow, offset);
 +                if (prev != null) {
 +                    return setRowFocus(prev);
 +                } else {
 +                    VConsole.log("no previous available");
 +                }
 +            }
 +        }
 +
 +        return false;
 +    }
 +
 +    /**
 +     * Selects a row where the current selection head is
 +     * 
 +     * @param ctrlSelect
 +     *            Is the selection a ctrl+selection
 +     * @param shiftSelect
 +     *            Is the selection a shift+selection
 +     * @return Returns truw
 +     */
 +    private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) {
 +        if (focusedRow != null) {
 +            // Arrows moves the selection and clears previous selections
 +            if (isSelectable() && !ctrlSelect && !shiftSelect) {
 +                deselectAll();
 +                focusedRow.toggleSelection();
 +                selectionRangeStart = focusedRow;
 +            } else if (isSelectable() && ctrlSelect && !shiftSelect) {
 +                // Ctrl+arrows moves selection head
 +                selectionRangeStart = focusedRow;
 +                // No selection, only selection head is moved
 +            } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) {
 +                // Shift+arrows selection selects a range
 +                focusedRow.toggleShiftSelection(shiftSelect);
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Sends the selection to the server if changed since the last update/visit.
 +     */
 +    protected void sendSelectedRows() {
 +        sendSelectedRows(immediate);
 +    }
 +
 +    /**
 +     * Sends the selection to the server if it has been changed since the last
 +     * update/visit.
 +     * 
 +     * @param immediately
 +     *            set to true to immediately send the rows
 +     */
 +    protected void sendSelectedRows(boolean immediately) {
 +        // Don't send anything if selection has not changed
 +        if (!selectionChanged) {
 +            return;
 +        }
 +
 +        // Reset selection changed flag
 +        selectionChanged = false;
 +
 +        // Note: changing the immediateness of this might require changes to
 +        // "clickEvent" immediateness also.
 +        if (isMultiSelectModeDefault()) {
 +            // Convert ranges to a set of strings
 +            Set<String> ranges = new HashSet<String>();
 +            for (SelectionRange range : selectedRowRanges) {
 +                ranges.add(range.toString());
 +            }
 +
 +            // Send the selected row ranges
 +            client.updateVariable(paintableId, "selectedRanges",
 +                    ranges.toArray(new String[selectedRowRanges.size()]), false);
 +
 +            // clean selectedRowKeys so that they don't contain excess values
 +            for (Iterator<String> iterator = selectedRowKeys.iterator(); iterator
 +                    .hasNext();) {
 +                String key = iterator.next();
 +                VScrollTableRow renderedRowByKey = getRenderedRowByKey(key);
 +                if (renderedRowByKey != null) {
 +                    for (SelectionRange range : selectedRowRanges) {
 +                        if (range.inRange(renderedRowByKey)) {
 +                            iterator.remove();
 +                        }
 +                    }
 +                } else {
 +                    // orphaned selected key, must be in a range, ignore
 +                    iterator.remove();
 +                }
 +
 +            }
 +        }
 +
 +        // Send the selected rows
 +        client.updateVariable(paintableId, "selected",
 +                selectedRowKeys.toArray(new String[selectedRowKeys.size()]),
 +                immediately);
 +
 +    }
 +
 +    /**
 +     * Get the key that moves the selection head upwards. By default it is the
 +     * up arrow key but by overriding this you can change the key to whatever
 +     * you want.
 +     * 
 +     * @return The keycode of the key
 +     */
 +    protected int getNavigationUpKey() {
 +        return KeyCodes.KEY_UP;
 +    }
 +
 +    /**
 +     * Get the key that moves the selection head downwards. By default it is the
 +     * down arrow key but by overriding this you can change the key to whatever
 +     * you want.
 +     * 
 +     * @return The keycode of the key
 +     */
 +    protected int getNavigationDownKey() {
 +        return KeyCodes.KEY_DOWN;
 +    }
 +
 +    /**
 +     * Get the key that scrolls to the left in the table. By default it is the
 +     * left arrow key but by overriding this you can change the key to whatever
 +     * you want.
 +     * 
 +     * @return The keycode of the key
 +     */
 +    protected int getNavigationLeftKey() {
 +        return KeyCodes.KEY_LEFT;
 +    }
 +
 +    /**
 +     * Get the key that scroll to the right on the table. By default it is the
 +     * right arrow key but by overriding this you can change the key to whatever
 +     * you want.
 +     * 
 +     * @return The keycode of the key
 +     */
 +    protected int getNavigationRightKey() {
 +        return KeyCodes.KEY_RIGHT;
 +    }
 +
 +    /**
 +     * Get the key that selects an item in the table. By default it is the space
 +     * bar key but by overriding this you can change the key to whatever you
 +     * want.
 +     * 
 +     * @return
 +     */
 +    protected int getNavigationSelectKey() {
 +        return CHARCODE_SPACE;
 +    }
 +
 +    /**
 +     * Get the key the moves the selection one page up in the table. By default
 +     * this is the Page Up key but by overriding this you can change the key to
 +     * whatever you want.
 +     * 
 +     * @return
 +     */
 +    protected int getNavigationPageUpKey() {
 +        return KeyCodes.KEY_PAGEUP;
 +    }
 +
 +    /**
 +     * Get the key the moves the selection one page down in the table. By
 +     * default this is the Page Down key but by overriding this you can change
 +     * the key to whatever you want.
 +     * 
 +     * @return
 +     */
 +    protected int getNavigationPageDownKey() {
 +        return KeyCodes.KEY_PAGEDOWN;
 +    }
 +
 +    /**
 +     * Get the key the moves the selection to the beginning of the table. By
 +     * default this is the Home key but by overriding this you can change the
 +     * key to whatever you want.
 +     * 
 +     * @return
 +     */
 +    protected int getNavigationStartKey() {
 +        return KeyCodes.KEY_HOME;
 +    }
 +
 +    /**
 +     * Get the key the moves the selection to the end of the table. By default
 +     * this is the End key but by overriding this you can change the key to
 +     * whatever you want.
 +     * 
 +     * @return
 +     */
 +    protected int getNavigationEndKey() {
 +        return KeyCodes.KEY_END;
 +    }
 +
 +    void initializeRows(UIDL uidl, UIDL rowData) {
 +        if (scrollBody != null) {
 +            scrollBody.removeFromParent();
 +        }
 +        scrollBody = createScrollBody();
 +
 +        scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"),
 +                uidl.getIntAttribute("rows"));
 +        scrollBodyPanel.add(scrollBody);
 +
 +        // New body starts scrolled to the left, make sure the header and footer
 +        // are also scrolled to the left
 +        tHead.setHorizontalScrollPosition(0);
 +        tFoot.setHorizontalScrollPosition(0);
 +
 +        initialContentReceived = true;
 +        sizeNeedsInit = true;
 +        scrollBody.restoreRowVisibility();
 +    }
 +
 +    void updateColumnProperties(UIDL uidl) {
 +        updateColumnOrder(uidl);
 +
 +        updateCollapsedColumns(uidl);
 +
 +        UIDL vc = uidl.getChildByTagName("visiblecolumns");
 +        if (vc != null) {
 +            tHead.updateCellsFromUIDL(vc);
 +            tFoot.updateCellsFromUIDL(vc);
 +        }
 +
 +        updateHeader(uidl.getStringArrayAttribute("vcolorder"));
 +        updateFooter(uidl.getStringArrayAttribute("vcolorder"));
 +    }
 +
 +    private void updateCollapsedColumns(UIDL uidl) {
 +        if (uidl.hasVariable("collapsedcolumns")) {
 +            tHead.setColumnCollapsingAllowed(true);
 +            collapsedColumns = uidl
 +                    .getStringArrayVariableAsSet("collapsedcolumns");
 +        } else {
 +            tHead.setColumnCollapsingAllowed(false);
 +        }
 +    }
 +
 +    private void updateColumnOrder(UIDL uidl) {
 +        if (uidl.hasVariable("columnorder")) {
 +            columnReordering = true;
 +            columnOrder = uidl.getStringArrayVariable("columnorder");
 +        } else {
 +            columnReordering = false;
 +            columnOrder = null;
 +        }
 +    }
 +
 +    boolean selectSelectedRows(UIDL uidl) {
 +        boolean keyboardSelectionOverRowFetchInProgress = false;
 +
 +        if (uidl.hasVariable("selected")) {
 +            final Set<String> selectedKeys = uidl
 +                    .getStringArrayVariableAsSet("selected");
 +            if (scrollBody != null) {
 +                Iterator<Widget> iterator = scrollBody.iterator();
 +                while (iterator.hasNext()) {
 +                    /*
 +                     * Make the focus reflect to the server side state unless we
 +                     * are currently selecting multiple rows with keyboard.
 +                     */
 +                    VScrollTableRow row = (VScrollTableRow) iterator.next();
 +                    boolean selected = selectedKeys.contains(row.getKey());
 +                    if (!selected
 +                            && unSyncedselectionsBeforeRowFetch != null
 +                            && unSyncedselectionsBeforeRowFetch.contains(row
 +                                    .getKey())) {
 +                        selected = true;
 +                        keyboardSelectionOverRowFetchInProgress = true;
 +                    }
 +                    if (selected != row.isSelected()) {
 +                        row.toggleSelection();
 +                        if (!isSingleSelectMode() && !selected) {
 +                            // Update selection range in case a row is
 +                            // unselected from the middle of a range - #8076
 +                            removeRowFromUnsentSelectionRanges(row);
 +                        }
 +                    }
 +                }
 +            }
 +        }
 +        unSyncedselectionsBeforeRowFetch = null;
 +        return keyboardSelectionOverRowFetchInProgress;
 +    }
 +
 +    void updateSortingProperties(UIDL uidl) {
 +        oldSortColumn = sortColumn;
 +        if (uidl.hasVariable("sortascending")) {
 +            sortAscending = uidl.getBooleanVariable("sortascending");
 +            sortColumn = uidl.getStringVariable("sortcolumn");
 +        }
 +    }
 +
 +    void resizeSortedColumnForSortIndicator() {
 +        // Force recalculation of the captionContainer element inside the header
 +        // cell to accomodate for the size of the sort arrow.
 +        HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn);
 +        if (sortedHeader != null) {
 +            tHead.resizeCaptionContainer(sortedHeader);
 +        }
 +        // Also recalculate the width of the captionContainer element in the
 +        // previously sorted header, since this now has more room.
 +        HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn);
 +        if (oldSortedHeader != null) {
 +            tHead.resizeCaptionContainer(oldSortedHeader);
 +        }
 +    }
 +
 +    void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
 +        firstvisible = uidl.hasVariable("firstvisible") ? uidl
 +                .getIntVariable("firstvisible") : 0;
 +        if (firstvisible != lastRequestedFirstvisible && scrollBody != null) {
 +            // received 'surprising' firstvisible from server: scroll there
 +            firstRowInViewPort = firstvisible;
 +            scrollBodyPanel
 +                    .setScrollPosition(measureRowHeightOffset(firstvisible));
 +        }
 +    }
 +
 +    protected int measureRowHeightOffset(int rowIx) {
 +        return (int) (rowIx * scrollBody.getRowHeight());
 +    }
 +
 +    void updatePageLength(UIDL uidl) {
 +        int oldPageLength = pageLength;
 +        if (uidl.hasAttribute("pagelength")) {
 +            pageLength = uidl.getIntAttribute("pagelength");
 +        } else {
 +            // pagelenght is "0" meaning scrolling is turned off
 +            pageLength = totalRows;
 +        }
 +
 +        if (oldPageLength != pageLength && initializedAndAttached) {
 +            // page length changed, need to update size
 +            sizeNeedsInit = true;
 +        }
 +    }
 +
 +    void updateSelectionProperties(UIDL uidl, ComponentState state,
 +            boolean readOnly) {
 +        setMultiSelectMode(uidl.hasAttribute("multiselectmode") ? uidl
 +                .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT);
 +
 +        nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl
 +                .getBooleanAttribute("nsa") : true;
 +
 +        if (uidl.hasAttribute("selectmode")) {
 +            if (readOnly) {
 +                selectMode = SelectMode.NONE;
 +            } else if (uidl.getStringAttribute("selectmode").equals("multi")) {
 +                selectMode = SelectMode.MULTI;
 +            } else if (uidl.getStringAttribute("selectmode").equals("single")) {
 +                selectMode = SelectMode.SINGLE;
 +            } else {
 +                selectMode = SelectMode.NONE;
 +            }
 +        }
 +    }
 +
 +    void updateDragMode(UIDL uidl) {
 +        dragmode = uidl.hasAttribute("dragmode") ? uidl
 +                .getIntAttribute("dragmode") : 0;
 +        if (BrowserInfo.get().isIE()) {
 +            if (dragmode > 0) {
 +                getElement().setPropertyJSO("onselectstart",
 +                        getPreventTextSelectionIEHack());
 +            } else {
 +                getElement().setPropertyJSO("onselectstart", null);
 +            }
 +        }
 +    }
 +
 +    protected void updateTotalRows(UIDL uidl) {
 +        int newTotalRows = uidl.getIntAttribute("totalrows");
 +        if (newTotalRows != getTotalRows()) {
 +            if (scrollBody != null) {
 +                if (getTotalRows() == 0) {
 +                    tHead.clear();
 +                    tFoot.clear();
 +                }
 +                initializedAndAttached = false;
 +                initialContentReceived = false;
 +                isNewBody = true;
 +            }
 +            setTotalRows(newTotalRows);
 +        }
 +    }
 +
 +    protected void setTotalRows(int newTotalRows) {
 +        totalRows = newTotalRows;
 +    }
 +
 +    public int getTotalRows() {
 +        return totalRows;
 +    }
 +
 +    void focusRowFromBody() {
 +        if (selectedRowKeys.size() == 1) {
 +            // try to focus a row currently selected and in viewport
 +            String selectedRowKey = selectedRowKeys.iterator().next();
 +            if (selectedRowKey != null) {
 +                VScrollTableRow renderedRow = getRenderedRowByKey(selectedRowKey);
 +                if (renderedRow == null || !renderedRow.isInViewPort()) {
 +                    setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
 +                } else {
 +                    setRowFocus(renderedRow);
 +                }
 +            }
 +        } else {
 +            // multiselect mode
 +            setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
 +        }
 +    }
 +
 +    protected VScrollTableBody createScrollBody() {
 +        return new VScrollTableBody();
 +    }
 +
 +    /**
 +     * Selects the last row visible in the table
 +     * 
 +     * @param focusOnly
 +     *            Should the focus only be moved to the last row
 +     */
 +    void selectLastRenderedRowInViewPort(boolean focusOnly) {
 +        int index = firstRowInViewPort + getFullyVisibleRowCount();
 +        VScrollTableRow lastRowInViewport = scrollBody.getRowByRowIndex(index);
 +        if (lastRowInViewport == null) {
 +            // this should not happen in normal situations (white space at the
 +            // end of viewport). Select the last rendered as a fallback.
 +            lastRowInViewport = scrollBody.getRowByRowIndex(scrollBody
 +                    .getLastRendered());
 +            if (lastRowInViewport == null) {
 +                return; // empty table
 +            }
 +        }
 +        setRowFocus(lastRowInViewport);
 +        if (!focusOnly) {
 +            selectFocusedRow(false, multiselectPending);
 +            sendSelectedRows();
 +        }
 +    }
 +
 +    /**
 +     * Selects the first row visible in the table
 +     * 
 +     * @param focusOnly
 +     *            Should the focus only be moved to the first row
 +     */
 +    void selectFirstRenderedRowInViewPort(boolean focusOnly) {
 +        int index = firstRowInViewPort;
 +        VScrollTableRow firstInViewport = scrollBody.getRowByRowIndex(index);
 +        if (firstInViewport == null) {
 +            // this should not happen in normal situations
 +            return;
 +        }
 +        setRowFocus(firstInViewport);
 +        if (!focusOnly) {
 +            selectFocusedRow(false, multiselectPending);
 +            sendSelectedRows();
 +        }
 +    }
 +
 +    void setCacheRateFromUIDL(UIDL uidl) {
 +        setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr")
 +                : CACHE_RATE_DEFAULT);
 +    }
 +
 +    private void setCacheRate(double d) {
 +        if (cache_rate != d) {
 +            cache_rate = d;
 +            cache_react_rate = 0.75 * d;
 +        }
 +    }
 +
 +    void updateActionMap(UIDL mainUidl) {
 +        UIDL actionsUidl = mainUidl.getChildByTagName("actions");
 +        if (actionsUidl == null) {
 +            return;
 +        }
 +
 +        final Iterator<?> it = actionsUidl.getChildIterator();
 +        while (it.hasNext()) {
 +            final UIDL action = (UIDL) it.next();
 +            final String key = action.getStringAttribute("key");
 +            final String caption = action.getStringAttribute("caption");
 +            actionMap.put(key + "_c", caption);
 +            if (action.hasAttribute("icon")) {
 +                // TODO need some uri handling ??
 +                actionMap.put(key + "_i", client.translateVaadinUri(action
 +                        .getStringAttribute("icon")));
 +            } else {
 +                actionMap.remove(key + "_i");
 +            }
 +        }
 +
 +    }
 +
 +    public String getActionCaption(String actionKey) {
 +        return actionMap.get(actionKey + "_c");
 +    }
 +
 +    public String getActionIcon(String actionKey) {
 +        return actionMap.get(actionKey + "_i");
 +    }
 +
 +    private void updateHeader(String[] strings) {
 +        if (strings == null) {
 +            return;
 +        }
 +
 +        int visibleCols = strings.length;
 +        int colIndex = 0;
 +        if (showRowHeaders) {
 +            tHead.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
 +            visibleCols++;
 +            visibleColOrder = new String[visibleCols];
 +            visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY;
 +            colIndex++;
 +        } else {
 +            visibleColOrder = new String[visibleCols];
 +            tHead.removeCell(ROW_HEADER_COLUMN_KEY);
 +        }
 +
 +        int i;
 +        for (i = 0; i < strings.length; i++) {
 +            final String cid = strings[i];
 +            visibleColOrder[colIndex] = cid;
 +            tHead.enableColumn(cid, colIndex);
 +            colIndex++;
 +        }
 +
 +        tHead.setVisible(showColHeaders);
 +        setContainerHeight();
 +
 +    }
 +
 +    /**
 +     * Updates footers.
 +     * <p>
 +     * Update headers whould be called before this method is called!
 +     * </p>
 +     * 
 +     * @param strings
 +     */
 +    private void updateFooter(String[] strings) {
 +        if (strings == null) {
 +            return;
 +        }
 +
 +        // Add dummy column if row headers are present
 +        int colIndex = 0;
 +        if (showRowHeaders) {
 +            tFoot.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
 +            colIndex++;
 +        } else {
 +            tFoot.removeCell(ROW_HEADER_COLUMN_KEY);
 +        }
 +
 +        int i;
 +        for (i = 0; i < strings.length; i++) {
 +            final String cid = strings[i];
 +            tFoot.enableColumn(cid, colIndex);
 +            colIndex++;
 +        }
 +
 +        tFoot.setVisible(showColFooters);
 +    }
 +
 +    /**
 +     * @param uidl
 +     *            which contains row data
 +     * @param firstRow
 +     *            first row in data set
 +     * @param reqRows
 +     *            amount of rows in data set
 +     */
 +    void updateBody(UIDL uidl, int firstRow, int reqRows) {
 +        if (uidl == null || reqRows < 1) {
 +            // container is empty, remove possibly existing rows
 +            if (firstRow <= 0) {
 +                while (scrollBody.getLastRendered() > scrollBody.firstRendered) {
 +                    scrollBody.unlinkRow(false);
 +                }
 +                scrollBody.unlinkRow(false);
 +            }
 +            return;
 +        }
 +
 +        scrollBody.renderRows(uidl, firstRow, reqRows);
 +
 +        discardRowsOutsideCacheWindow();
 +    }
 +
 +    void updateRowsInBody(UIDL partialRowUpdates) {
 +        if (partialRowUpdates == null) {
 +            return;
 +        }
 +        int firstRowIx = partialRowUpdates.getIntAttribute("firsturowix");
 +        int count = partialRowUpdates.getIntAttribute("numurows");
 +        scrollBody.unlinkRows(firstRowIx, count);
 +        scrollBody.insertRows(partialRowUpdates, firstRowIx, count);
 +    }
 +
 +    /**
 +     * Updates the internal cache by unlinking rows that fall outside of the
 +     * caching window.
 +     */
 +    protected void discardRowsOutsideCacheWindow() {
 +        int firstRowToKeep = (int) (firstRowInViewPort - pageLength
 +                * cache_rate);
 +        int lastRowToKeep = (int) (firstRowInViewPort + pageLength + pageLength
 +                * cache_rate);
 +        debug("Client side calculated cache rows to keep: " + firstRowToKeep
 +                + "-" + lastRowToKeep);
 +
 +        if (serverCacheFirst != -1) {
 +            firstRowToKeep = serverCacheFirst;
 +            lastRowToKeep = serverCacheLast;
 +            debug("Server cache rows that override: " + serverCacheFirst + "-"
 +                    + serverCacheLast);
 +            if (firstRowToKeep < scrollBody.getFirstRendered()
 +                    || lastRowToKeep > scrollBody.getLastRendered()) {
 +                debug("*** Server wants us to keep " + serverCacheFirst + "-"
 +                        + serverCacheLast + " but we only have rows "
 +                        + scrollBody.getFirstRendered() + "-"
 +                        + scrollBody.getLastRendered() + " rendered!");
 +            }
 +        }
 +        discardRowsOutsideOf(firstRowToKeep, lastRowToKeep);
 +
 +        scrollBody.fixSpacers();
 +
 +        scrollBody.restoreRowVisibility();
 +    }
 +
 +    private void discardRowsOutsideOf(int optimalFirstRow, int optimalLastRow) {
 +        /*
 +         * firstDiscarded and lastDiscarded are only calculated for debug
 +         * purposes
 +         */
 +        int firstDiscarded = -1, lastDiscarded = -1;
 +        boolean cont = true;
 +        while (cont && scrollBody.getLastRendered() > optimalFirstRow
 +                && scrollBody.getFirstRendered() < optimalFirstRow) {
 +            if (firstDiscarded == -1) {
 +                firstDiscarded = scrollBody.getFirstRendered();
 +            }
 +
 +            // removing row from start
 +            cont = scrollBody.unlinkRow(true);
 +        }
 +        if (firstDiscarded != -1) {
 +            lastDiscarded = scrollBody.getFirstRendered() - 1;
 +            debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
 +        }
 +        firstDiscarded = lastDiscarded = -1;
 +
 +        cont = true;
 +        while (cont && scrollBody.getLastRendered() > optimalLastRow) {
 +            if (lastDiscarded == -1) {
 +                lastDiscarded = scrollBody.getLastRendered();
 +            }
 +
 +            // removing row from the end
 +            cont = scrollBody.unlinkRow(false);
 +        }
 +        if (lastDiscarded != -1) {
 +            firstDiscarded = scrollBody.getLastRendered() + 1;
 +            debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
 +        }
 +
 +        debug("Now in cache: " + scrollBody.getFirstRendered() + "-"
 +                + scrollBody.getLastRendered());
 +    }
 +
 +    /**
 +     * Inserts rows in the table body or removes them from the table body based
 +     * on the commands in the UIDL.
 +     * 
 +     * @param partialRowAdditions
 +     *            the UIDL containing row updates.
 +     */
 +    protected void addAndRemoveRows(UIDL partialRowAdditions) {
 +        if (partialRowAdditions == null) {
 +            return;
 +        }
 +        if (partialRowAdditions.hasAttribute("hide")) {
 +            scrollBody.unlinkAndReindexRows(
 +                    partialRowAdditions.getIntAttribute("firstprowix"),
 +                    partialRowAdditions.getIntAttribute("numprows"));
 +            scrollBody.ensureCacheFilled();
 +        } else {
 +            if (partialRowAdditions.hasAttribute("delbelow")) {
 +                scrollBody.insertRowsDeleteBelow(partialRowAdditions,
 +                        partialRowAdditions.getIntAttribute("firstprowix"),
 +                        partialRowAdditions.getIntAttribute("numprows"));
 +            } else {
 +                scrollBody.insertAndReindexRows(partialRowAdditions,
 +                        partialRowAdditions.getIntAttribute("firstprowix"),
 +                        partialRowAdditions.getIntAttribute("numprows"));
 +            }
 +        }
 +
 +        discardRowsOutsideCacheWindow();
 +    }
 +
 +    /**
 +     * 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 (ROW_HEADER_COLUMN_KEY.equals(colKey)) {
 +            return 0;
 +        }
 +        for (int i = 0; i < visibleColOrder.length; i++) {
 +            if (visibleColOrder[i].equals(colKey)) {
 +                return i;
 +            }
 +        }
 +        return -1;
 +    }
 +
 +    private boolean isMultiSelectModeSimple() {
 +        return selectMode == SelectMode.MULTI
 +                && multiselectmode == MULTISELECT_MODE_SIMPLE;
 +    }
 +
 +    private boolean isSingleSelectMode() {
 +        return selectMode == SelectMode.SINGLE;
 +    }
 +
 +    private boolean isMultiSelectModeAny() {
 +        return selectMode == SelectMode.MULTI;
 +    }
 +
 +    private boolean isMultiSelectModeDefault() {
 +        return selectMode == SelectMode.MULTI
 +                && multiselectmode == MULTISELECT_MODE_DEFAULT;
 +    }
 +
 +    private void setMultiSelectMode(int multiselectmode) {
 +        if (BrowserInfo.get().isTouchDevice()) {
 +            // Always use the simple mode for touch devices that do not have
 +            // shift/ctrl keys
 +            this.multiselectmode = MULTISELECT_MODE_SIMPLE;
 +        } else {
 +            this.multiselectmode = multiselectmode;
 +        }
 +
 +    }
 +
 +    protected boolean isSelectable() {
 +        return selectMode.getId() > SelectMode.NONE.getId();
 +    }
 +
 +    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, boolean isDefinedWidth) {
 +        final HeaderCell hcell = tHead.getHeaderCell(colIndex);
 +
 +        // Make sure that the column grows to accommodate the sort indicator if
 +        // necessary.
 +        if (w < hcell.getMinWidth()) {
 +            w = hcell.getMinWidth();
 +        }
 +
 +        // Set header column width
 +        hcell.setWidth(w, isDefinedWidth);
 +
 +        // Ensure indicators have been taken into account
 +        tHead.resizeCaptionContainer(hcell);
 +
 +        // Set body column width
 +        scrollBody.setColWidth(colIndex, w);
 +
 +        // Set footer column width
 +        FooterCell fcell = tFoot.getFooterCell(colIndex);
 +        fcell.setWidth(w, isDefinedWidth);
 +    }
 +
 +    private int getColWidth(String colKey) {
 +        return tHead.getHeaderCell(colKey).getWidth();
 +    }
 +
 +    /**
 +     * Get a rendered row by its key
 +     * 
 +     * @param key
 +     *            The key to search with
 +     * @return
 +     */
 +    public VScrollTableRow getRenderedRowByKey(String key) {
 +        if (scrollBody != null) {
 +            final Iterator<Widget> it = scrollBody.iterator();
 +            VScrollTableRow r = null;
 +            while (it.hasNext()) {
 +                r = (VScrollTableRow) it.next();
 +                if (r.getKey().equals(key)) {
 +                    return r;
 +                }
 +            }
 +        }
 +        return null;
 +    }
 +
 +    /**
 +     * Returns the next row to the given row
 +     * 
 +     * @param row
 +     *            The row to calculate from
 +     * 
 +     * @return The next row or null if no row exists
 +     */
 +    private VScrollTableRow getNextRow(VScrollTableRow row, int offset) {
 +        final Iterator<Widget> it = scrollBody.iterator();
 +        VScrollTableRow r = null;
 +        while (it.hasNext()) {
 +            r = (VScrollTableRow) it.next();
 +            if (r == row) {
 +                r = null;
 +                while (offset >= 0 && it.hasNext()) {
 +                    r = (VScrollTableRow) it.next();
 +                    offset--;
 +                }
 +                return r;
 +            }
 +        }
 +
 +        return null;
 +    }
 +
 +    /**
 +     * Returns the previous row from the given row
 +     * 
 +     * @param row
 +     *            The row to calculate from
 +     * @return The previous row or null if no row exists
 +     */
 +    private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) {
 +        final Iterator<Widget> it = scrollBody.iterator();
 +        final Iterator<Widget> offsetIt = scrollBody.iterator();
 +        VScrollTableRow r = null;
 +        VScrollTableRow prev = null;
 +        while (it.hasNext()) {
 +            r = (VScrollTableRow) it.next();
 +            if (offset < 0) {
 +                prev = (VScrollTableRow) offsetIt.next();
 +            }
 +            if (r == row) {
 +                return prev;
 +            }
 +            offset--;
 +        }
 +
 +        return null;
 +    }
 +
 +    protected void reOrderColumn(String columnKey, int newIndex) {
 +
 +        final int oldIndex = getColIndexByKey(columnKey);
 +
 +        // Change header order
 +        tHead.moveCell(oldIndex, newIndex);
 +
 +        // Change body order
 +        scrollBody.moveCol(oldIndex, newIndex);
 +
 +        // Change footer order
 +        tFoot.moveCell(oldIndex, newIndex);
 +
 +        /*
 +         * Build new columnOrder and update it to server Note that columnOrder
 +         * also contains collapsed columns so we cannot directly build it from
 +         * cells vector Loop the old columnOrder and append in order to new
 +         * array unless on moved columnKey. On new index also put the moved key
 +         * i == index on columnOrder, j == index on newOrder
 +         */
 +        final String oldKeyOnNewIndex = visibleColOrder[newIndex];
 +        if (showRowHeaders) {
 +            newIndex--; // columnOrder don't have rowHeader
 +        }
 +        // add back hidden rows,
 +        for (int i = 0; i < columnOrder.length; i++) {
 +            if (columnOrder[i].equals(oldKeyOnNewIndex)) {
 +                break; // break loop at target
 +            }
 +            if (isCollapsedColumn(columnOrder[i])) {
 +                newIndex++;
 +            }
 +        }
 +        // finally we can build the new columnOrder for server
 +        final String[] newOrder = new String[columnOrder.length];
 +        for (int i = 0, j = 0; j < newOrder.length; i++) {
 +            if (j == newIndex) {
 +                newOrder[j] = columnKey;
 +                j++;
 +            }
 +            if (i == columnOrder.length) {
 +                break;
 +            }
 +            if (columnOrder[i].equals(columnKey)) {
 +                continue;
 +            }
 +            newOrder[j] = columnOrder[i];
 +            j++;
 +        }
 +        columnOrder = newOrder;
 +        // also update visibleColumnOrder
 +        int i = showRowHeaders ? 1 : 0;
 +        for (int j = 0; j < newOrder.length; j++) {
 +            final String cid = newOrder[j];
 +            if (!isCollapsedColumn(cid)) {
 +                visibleColOrder[i++] = cid;
 +            }
 +        }
 +        client.updateVariable(paintableId, "columnorder", columnOrder, false);
 +        if (client.hasEventListeners(this, COLUMN_REORDER_EVENT_ID)) {
 +            client.sendPendingVariableChanges();
 +        }
 +    }
 +
 +    @Override
 +    protected void onDetach() {
 +        rowRequestHandler.cancel();
 +        super.onDetach();
 +        // ensure that scrollPosElement will be detached
 +        if (scrollPositionElement != null) {
 +            final Element parent = DOM.getParent(scrollPositionElement);
 +            if (parent != null) {
 +                DOM.removeChild(parent, scrollPositionElement);
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Run only once when component is attached and received its initial
 +     * content. This function:
 +     * 
 +     * * Syncs headers and bodys "natural widths and saves the values.
 +     * 
 +     * * Sets proper width and height
 +     * 
 +     * * Makes deferred request to get some cache rows
 +     */
 +    void sizeInit() {
 +        sizeNeedsInit = false;
 +
 +        scrollBody.setContainerHeight();
 +
 +        /*
 +         * 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<Widget> headCells = tHead.iterator();
 +        Iterator<Widget> footCells = tFoot.iterator();
 +        int i = 0;
 +        int totalExplicitColumnsWidths = 0;
 +        int total = 0;
 +        float expandRatioDivider = 0;
 +
 +        final int[] widths = new int[tHead.visibleCells.size()];
 +
 +        tHead.enableBrowserIntelligence();
 +        tFoot.enableBrowserIntelligence();
 +
 +        // first loop: collect natural widths
 +        while (headCells.hasNext()) {
 +            final HeaderCell hCell = (HeaderCell) headCells.next();
 +            final FooterCell fCell = (FooterCell) footCells.next();
 +            int w = hCell.getWidth();
 +            if (hCell.isDefinedWidth()) {
 +                // server has defined column width explicitly
 +                totalExplicitColumnsWidths += w;
 +            } else {
 +                if (hCell.getExpandRatio() > 0) {
 +                    expandRatioDivider += hCell.getExpandRatio();
 +                    w = 0;
 +                } else {
 +                    // get and store greater of header width and column width,
 +                    // and
 +                    // store it as a minimumn natural col width
 +                    int headerWidth = hCell.getNaturalColumnWidth(i);
 +                    int footerWidth = fCell.getNaturalColumnWidth(i);
 +                    w = headerWidth > footerWidth ? headerWidth : footerWidth;
 +                }
 +                hCell.setNaturalMinimumColumnWidth(w);
 +                fCell.setNaturalMinimumColumnWidth(w);
 +            }
 +            widths[i] = w;
 +            total += w;
 +            i++;
 +        }
 +
 +        tHead.disableBrowserIntelligence();
 +        tFoot.disableBrowserIntelligence();
 +
 +        boolean willHaveScrollbarz = willHaveScrollbars();
 +
 +        // fix "natural" width if width not set
 +        if (isDynamicWidth()) {
 +            int w = total;
 +            w += scrollBody.getCellExtraWidth() * visibleColOrder.length;
 +            if (willHaveScrollbarz) {
 +                w += Util.getNativeScrollbarSize();
 +            }
 +            setContentWidth(w);
 +        }
 +
 +        int availW = scrollBody.getAvailableWidth();
 +        if (BrowserInfo.get().isIE()) {
 +            // Hey IE, are you really sure about this?
 +            availW = scrollBody.getAvailableWidth();
 +        }
 +        availW -= scrollBody.getCellExtraWidth() * visibleColOrder.length;
 +
 +        if (willHaveScrollbarz) {
 +            availW -= Util.getNativeScrollbarSize();
 +        }
 +
 +        // TODO refactor this code to be the same as in resize timer
 +        boolean needsReLayout = false;
 +
 +        if (availW > total) {
 +            // natural size is smaller than available space
 +            final int extraSpace = availW - total;
 +            final int totalWidthR = total - totalExplicitColumnsWidths;
 +            int checksum = 0;
 +            needsReLayout = true;
 +
 +            if (extraSpace == 1) {
 +                // We cannot divide one single pixel so we give it the first
 +                // undefined column
 +                headCells = tHead.iterator();
 +                i = 0;
 +                checksum = availW;
 +                while (headCells.hasNext()) {
 +                    HeaderCell hc = (HeaderCell) headCells.next();
 +                    if (!hc.isDefinedWidth()) {
 +                        widths[i]++;
 +                        break;
 +                    }
 +                    i++;
 +                }
 +
 +            } else if (expandRatioDivider > 0) {
 +                // visible columns have some active expand ratios, excess
 +                // space is divided according to them
 +                headCells = tHead.iterator();
 +                i = 0;
 +                while (headCells.hasNext()) {
 +                    HeaderCell hCell = (HeaderCell) headCells.next();
 +                    if (hCell.getExpandRatio() > 0) {
 +                        int w = widths[i];
 +                        final int newSpace = Math.round((extraSpace * (hCell
 +                                .getExpandRatio() / expandRatioDivider)));
 +                        w += newSpace;
 +                        widths[i] = w;
 +                    }
 +                    checksum += widths[i];
 +                    i++;
 +                }
 +            } else if (totalWidthR > 0) {
 +                // no expand ratios defined, we will share extra space
 +                // relatively to "natural widths" among those without
 +                // explicit width
 +                headCells = tHead.iterator();
 +                i = 0;
 +                while (headCells.hasNext()) {
 +                    HeaderCell hCell = (HeaderCell) headCells.next();
 +                    if (!hCell.isDefinedWidth()) {
 +                        int w = widths[i];
 +                        final int newSpace = Math.round((float) extraSpace
 +                                * (float) w / totalWidthR);
 +                        w += newSpace;
 +                        widths[i] = w;
 +                    }
 +                    checksum += widths[i];
 +                    i++;
 +                }
 +            }
 +
 +            if (extraSpace > 0 && checksum != availW) {
 +                /*
 +                 * There might be in some cases a rounding error of 1px when
 +                 * extra space is divided so if there is one then we give the
 +                 * first undefined column 1 more pixel
 +                 */
 +                headCells = tHead.iterator();
 +                i = 0;
 +                while (headCells.hasNext()) {
 +                    HeaderCell hc = (HeaderCell) headCells.next();
 +                    if (!hc.isDefinedWidth()) {
 +                        widths[i] += availW - checksum;
 +                        break;
 +                    }
 +                    i++;
 +                }
 +            }
 +
 +        } else {
 +            // bodys size will be more than available and scrollbar will appear
 +        }
 +
 +        // last loop: set possibly modified values or reset if new tBody
 +        i = 0;
 +        headCells = tHead.iterator();
 +        while (headCells.hasNext()) {
 +            final HeaderCell hCell = (HeaderCell) headCells.next();
 +            if (isNewBody || hCell.getWidth() == -1) {
 +                final int w = widths[i];
 +                setColWidth(i, w, false);
 +            }
 +            i++;
 +        }
 +
 +        initializedAndAttached = true;
 +
 +        if (needsReLayout) {
 +            scrollBody.reLayoutComponents();
 +        }
 +
 +        updatePageLength();
 +
 +        /*
 +         * Fix "natural" height if height is not set. This must be after width
 +         * fixing so the components' widths have been adjusted.
 +         */
 +        if (isDynamicHeight()) {
 +            /*
 +             * We must force an update of the row height as this point as it
 +             * might have been (incorrectly) calculated earlier
 +             */
 +
 +            int bodyHeight;
 +            if (pageLength == totalRows) {
 +                /*
 +                 * A hack to support variable height rows when paging is off.
 +                 * Generally this is not supported by scrolltable. We want to
 +                 * show all rows so the bodyHeight should be equal to the table
 +                 * height.
 +                 */
 +                // int bodyHeight = scrollBody.getOffsetHeight();
 +                bodyHeight = scrollBody.getRequiredHeight();
 +            } else {
 +                bodyHeight = (int) Math.round(scrollBody.getRowHeight(true)
 +                        * pageLength);
 +            }
 +            boolean needsSpaceForHorizontalSrollbar = (total > availW);
 +            if (needsSpaceForHorizontalSrollbar) {
 +                bodyHeight += Util.getNativeScrollbarSize();
 +            }
 +            scrollBodyPanel.setHeight(bodyHeight + "px");
 +            Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
 +        }
 +
 +        isNewBody = false;
 +
 +        if (firstvisible > 0) {
 +            // FIXME #7607
 +            // Originally deferred due to Firefox oddities which should not
 +            // occur any more. Currently deferring breaks Webkit scrolling with
 +            // relative-height tables, but not deferring instead breaks tables
 +            // with explicit page length.
 +            Scheduler.get().scheduleDeferred(new Command() {
 +                public void execute() {
 +                    scrollBodyPanel
 +                            .setScrollPosition(measureRowHeightOffset(firstvisible));
 +                    firstRowInViewPort = firstvisible;
 +                }
 +            });
 +        }
 +
 +        if (enabled) {
 +            // Do we need cache rows
 +            if (scrollBody.getLastRendered() + 1 < firstRowInViewPort
 +                    + pageLength + (int) cache_react_rate * pageLength) {
 +                if (totalRows - 1 > scrollBody.getLastRendered()) {
 +                    // fetch cache rows
 +                    int firstInNewSet = scrollBody.getLastRendered() + 1;
 +                    rowRequestHandler.setReqFirstRow(firstInNewSet);
 +                    int lastInNewSet = (int) (firstRowInViewPort + pageLength + cache_rate
 +                            * pageLength);
 +                    if (lastInNewSet > totalRows - 1) {
 +                        lastInNewSet = totalRows - 1;
 +                    }
 +                    rowRequestHandler.setReqRows(lastInNewSet - firstInNewSet
 +                            + 1);
 +                    rowRequestHandler.deferRowFetch(1);
 +                }
 +            }
 +        }
 +
 +        /*
 +         * Ensures the column alignments are correct at initial loading. <br/>
 +         * (child components widths are correct)
 +         */
 +        scrollBody.reLayoutComponents();
 +        Scheduler.get().scheduleDeferred(new Command() {
 +            public void execute() {
 +                Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
 +            }
 +        });
 +    }
 +
 +    /**
 +     * Note, this method is not official api although declared as protected.
 +     * Extend at you own risk.
 +     * 
 +     * @return true if content area will have scrollbars visible.
 +     */
 +    protected boolean willHaveScrollbars() {
 +        if (isDynamicHeight()) {
 +            if (pageLength < totalRows) {
 +                return true;
 +            }
 +        } else {
 +            int fakeheight = (int) Math.round(scrollBody.getRowHeight()
 +                    * totalRows);
 +            int availableHeight = scrollBodyPanel.getElement().getPropertyInt(
 +                    "clientHeight");
 +            if (fakeheight > availableHeight) {
 +                return true;
 +            }
 +        }
 +        return false;
 +    }
 +
 +    private void announceScrollPosition() {
 +        if (scrollPositionElement == null) {
 +            scrollPositionElement = DOM.createDiv();
 +            scrollPositionElement.setClassName(CLASSNAME + "-scrollposition");
 +            scrollPositionElement.getStyle().setPosition(Position.ABSOLUTE);
 +            scrollPositionElement.getStyle().setDisplay(Display.NONE);
 +            getElement().appendChild(scrollPositionElement);
 +        }
 +
 +        Style style = scrollPositionElement.getStyle();
 +        style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX);
 +        style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX);
 +
 +        // indexes go from 1-totalRows, as rowheaders in index-mode indicate
 +        int last = (firstRowInViewPort + pageLength);
 +        if (last > totalRows) {
 +            last = totalRows;
 +        }
 +        scrollPositionElement.setInnerHTML("<span>" + (firstRowInViewPort + 1)
 +                + " &ndash; " + (last) + "..." + "</span>");
 +        style.setDisplay(Display.BLOCK);
 +    }
 +
 +    void hideScrollPositionAnnotation() {
 +        if (scrollPositionElement != null) {
 +            DOM.setStyleAttribute(scrollPositionElement, "display", "none");
 +        }
 +    }
 +
 +    boolean isScrollPositionVisible() {
 +        return scrollPositionElement != null
 +                && !scrollPositionElement.getStyle().getDisplay()
 +                        .equals(Display.NONE.toString());
 +    }
 +
 +    class RowRequestHandler extends Timer {
 +
 +        private int reqFirstRow = 0;
 +        private int reqRows = 0;
 +        private boolean isRunning = false;
 +
 +        public void deferRowFetch() {
 +            deferRowFetch(250);
 +        }
 +
 +        public boolean isRunning() {
 +            return isRunning;
 +        }
 +
 +        public void deferRowFetch(int msec) {
 +            isRunning = true;
 +            if (reqRows > 0 && reqFirstRow < totalRows) {
 +                schedule(msec);
 +
 +                // tell scroll position to user if currently "visible" rows are
 +                // not rendered
 +                if (totalRows > pageLength
 +                        && ((firstRowInViewPort + pageLength > scrollBody
 +                                .getLastRendered()) || (firstRowInViewPort < scrollBody
 +                                .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;
 +        }
 +
 +        @Override
 +        public void run() {
 +            if (client.hasActiveRequest() || navKeyDown) {
 +                // if client connection is busy, don't bother loading it more
 +                VConsole.log("Postponed rowfetch");
 +                schedule(250);
 +            } else {
 +
 +                int firstToBeRendered = scrollBody.firstRendered;
 +                if (reqFirstRow < firstToBeRendered) {
 +                    firstToBeRendered = reqFirstRow;
 +                } else if (firstRowInViewPort - (int) (cache_rate * pageLength) > firstToBeRendered) {
 +                    firstToBeRendered = firstRowInViewPort
 +                            - (int) (cache_rate * pageLength);
 +                    if (firstToBeRendered < 0) {
 +                        firstToBeRendered = 0;
 +                    }
 +                }
 +
 +                int lastToBeRendered = scrollBody.lastRendered;
 +
 +                if (reqFirstRow + reqRows - 1 > lastToBeRendered) {
 +                    lastToBeRendered = reqFirstRow + reqRows - 1;
 +                } else if (firstRowInViewPort + pageLength + pageLength
 +                        * cache_rate < lastToBeRendered) {
 +                    lastToBeRendered = (firstRowInViewPort + pageLength + (int) (pageLength * cache_rate));
 +                    if (lastToBeRendered >= totalRows) {
 +                        lastToBeRendered = totalRows - 1;
 +                    }
 +                    // due Safari 3.1 bug (see #2607), verify reqrows, original
 +                    // problem unknown, but this should catch the issue
 +                    if (reqFirstRow + reqRows - 1 > lastToBeRendered) {
 +                        reqRows = lastToBeRendered - reqFirstRow;
 +                    }
 +                }
 +
 +                client.updateVariable(paintableId, "firstToBeRendered",
 +                        firstToBeRendered, false);
 +
 +                client.updateVariable(paintableId, "lastToBeRendered",
 +                        lastToBeRendered, false);
 +                // remember which firstvisible we requested, in case the server
 +                // has
 +                // a differing opinion
 +                lastRequestedFirstvisible = firstRowInViewPort;
 +                client.updateVariable(paintableId, "firstvisible",
 +                        firstRowInViewPort, false);
 +                client.updateVariable(paintableId, "reqfirstrow", reqFirstRow,
 +                        false);
 +                client.updateVariable(paintableId, "reqrows", reqRows, true);
 +
 +                if (selectionChanged) {
 +                    unSyncedselectionsBeforeRowFetch = new HashSet<Object>(
 +                            selectedRowKeys);
 +                }
 +                isRunning = false;
 +            }
 +        }
 +
 +        public int getReqFirstRow() {
 +            return reqFirstRow;
 +        }
 +
 +        /**
 +         * Sends request to refresh content at this position.
 +         */
 +        public void refreshContent() {
 +            isRunning = true;
 +            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 {
 +
 +        Element td = DOM.createTD();
 +
 +        Element captionContainer = DOM.createDiv();
 +
 +        Element sortIndicator = DOM.createDiv();
 +
 +        Element colResizeWidget = DOM.createDiv();
 +
 +        Element floatingCopyOfHeaderCell;
 +
 +        private boolean sortable = false;
 +        private final String cid;
 +        private boolean dragging;
 +
 +        private int dragStartX;
 +        private int colIndex;
 +        private int originalWidth;
 +
 +        private boolean isResizing;
 +
 +        private int headerX;
 +
 +        private boolean moved;
 +
 +        private int closestSlot;
 +
 +        private int width = -1;
 +
 +        private int naturalWidth = -1;
 +
 +        private char align = ALIGN_LEFT;
 +
 +        boolean definedWidth = false;
 +
 +        private float expandRatio = 0;
 +
 +        private boolean sorted;
 +
 +        public void setSortable(boolean b) {
 +            sortable = b;
 +        }
 +
 +        /**
 +         * Makes room for the sorting indicator in case the column that the
 +         * header cell belongs to is sorted. This is done by resizing the width
 +         * of the caption container element by the correct amount
 +         */
 +        public void resizeCaptionContainer(int rightSpacing) {
 +            int captionContainerWidth = width
 +                    - colResizeWidget.getOffsetWidth() - rightSpacing;
 +
 +            if (td.getClassName().contains("-asc")
 +                    || td.getClassName().contains("-desc")) {
 +                // Leave room for the sort indicator
 +                captionContainerWidth -= sortIndicator.getOffsetWidth();
 +            }
 +
 +            if (captionContainerWidth < 0) {
 +                rightSpacing += captionContainerWidth;
 +                captionContainerWidth = 0;
 +            }
 +
 +            captionContainer.getStyle().setPropertyPx("width",
 +                    captionContainerWidth);
 +
 +            // Apply/Remove spacing if defined
 +            if (rightSpacing > 0) {
 +                colResizeWidget.getStyle().setMarginLeft(rightSpacing, Unit.PX);
 +            } else {
 +                colResizeWidget.getStyle().clearMarginLeft();
 +            }
 +        }
 +
 +        public void setNaturalMinimumColumnWidth(int w) {
 +            naturalWidth = w;
 +        }
 +
 +        public HeaderCell(String colId, String headerText) {
 +            cid = colId;
 +
 +            DOM.setElementProperty(colResizeWidget, "className", CLASSNAME
 +                    + "-resizer");
 +
 +            setText(headerText);
 +
 +            DOM.appendChild(td, colResizeWidget);
 +
 +            DOM.setElementProperty(sortIndicator, "className", CLASSNAME
 +                    + "-sort-indicator");
 +            DOM.appendChild(td, sortIndicator);
 +
 +            DOM.setElementProperty(captionContainer, "className", CLASSNAME
 +                    + "-caption-container");
 +
 +            // ensure no clipping initially (problem on column additions)
 +            DOM.setStyleAttribute(captionContainer, "overflow", "visible");
 +
 +            DOM.appendChild(td, captionContainer);
 +
 +            DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
 +                    | Event.ONCONTEXTMENU | Event.TOUCHEVENTS);
 +
 +            setElement(td);
 +
 +            setAlign(ALIGN_LEFT);
 +        }
 +
 +        public void disableAutoWidthCalculation() {
 +            definedWidth = true;
 +            expandRatio = 0;
 +        }
 +
 +        public void setWidth(int w, boolean ensureDefinedWidth) {
 +            if (ensureDefinedWidth) {
 +                definedWidth = true;
 +                // on column resize expand ratio becomes zero
 +                expandRatio = 0;
 +            }
 +            if (width == -1) {
 +                // go to default mode, clip content if necessary
 +                DOM.setStyleAttribute(captionContainer, "overflow", "");
 +            }
 +            width = w;
 +            if (w == -1) {
 +                DOM.setStyleAttribute(captionContainer, "width", "");
 +                setWidth("");
 +            } else {
 +                tHead.resizeCaptionContainer(this);
 +
 +                /*
 +                 * if we already have tBody, set the header width properly, if
 +                 * not defer it. IE will fail with complex float in table header
 +                 * unless TD width is not explicitly set.
 +                 */
 +                if (scrollBody != null) {
 +                    int tdWidth = width + scrollBody.getCellExtraWidth();
 +                    setWidth(tdWidth + "px");
 +                } else {
 +                    Scheduler.get().scheduleDeferred(new Command() {
 +                        public void execute() {
 +                            int tdWidth = width
 +                                    + scrollBody.getCellExtraWidth();
 +                            setWidth(tdWidth + "px");
 +                        }
 +                    });
 +                }
 +            }
 +        }
 +
 +        public void setUndefinedWidth() {
 +            definedWidth = false;
 +            setWidth(-1, false);
 +        }
 +
 +        /**
 +         * Detects if width is fixed by developer on server side or resized to
 +         * current width by user.
 +         * 
 +         * @return true if defined, false if "natural" width
 +         */
 +        public boolean isDefinedWidth() {
 +            return definedWidth && width >= 0;
 +        }
 +
 +        public int getWidth() {
 +            return width;
 +        }
 +
 +        public void setText(String headerText) {
 +            DOM.setInnerHTML(captionContainer, headerText);
 +        }
 +
 +        public String getColKey() {
 +            return cid;
 +        }
 +
 +        private void setSorted(boolean sorted) {
 +            this.sorted = 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.
 +         */
 +        @Override
 +        public void onBrowserEvent(Event event) {
 +            if (enabled && event != null) {
 +                if (isResizing
 +                        || event.getEventTarget().cast() == colResizeWidget) {
 +                    if (dragging
 +                            && (event.getTypeInt() == Event.ONMOUSEUP || event
 +                                    .getTypeInt() == Event.ONTOUCHEND)) {
 +                        // Handle releasing column header on spacer #5318
 +                        handleCaptionEvent(event);
 +                    } else {
 +                        onResizeEvent(event);
 +                    }
 +                } else {
 +                    /*
 +                     * Ensure focus before handling caption event. Otherwise
 +                     * variables changed from caption event may be before
 +                     * variables from other components that fire variables when
 +                     * they lose focus.
 +                     */
 +                    if (event.getTypeInt() == Event.ONMOUSEDOWN
 +                            || event.getTypeInt() == Event.ONTOUCHSTART) {
 +                        scrollBodyPanel.setFocus(true);
 +                    }
 +                    handleCaptionEvent(event);
 +                    boolean stopPropagation = true;
 +                    if (event.getTypeInt() == Event.ONCONTEXTMENU
 +                            && !client.hasEventListeners(VScrollTable.this,
 +                                    HEADER_CLICK_EVENT_ID)) {
 +                        // Prevent showing the browser's context menu only when
 +                        // there is a header click listener.
 +                        stopPropagation = false;
 +                    }
 +                    if (stopPropagation) {
 +                        event.stopPropagation();
 +                        event.preventDefault();
 +                    }
 +                }
 +            }
 +        }
 +
 +        private void createFloatingCopy() {
 +            floatingCopyOfHeaderCell = DOM.createDiv();
 +            DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td));
 +            floatingCopyOfHeaderCell = DOM
 +                    .getChild(floatingCopyOfHeaderCell, 2);
 +            DOM.setElementProperty(floatingCopyOfHeaderCell, "className",
 +                    CLASSNAME + "-header-drag");
 +            // otherwise might wrap or be cut if narrow column
 +            DOM.setStyleAttribute(floatingCopyOfHeaderCell, "width", "auto");
 +            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;
 +        }
 +
 +        /**
 +         * Fires a header click event after the user has clicked a column header
 +         * cell
 +         * 
 +         * @param event
 +         *            The click event
 +         */
 +        private void fireHeaderClickedEvent(Event event) {
 +            if (client.hasEventListeners(VScrollTable.this,
 +                    HEADER_CLICK_EVENT_ID)) {
 +                MouseEventDetails details = MouseEventDetailsBuilder
 +                        .buildMouseEventDetails(event);
 +                client.updateVariable(paintableId, "headerClickEvent",
 +                        details.toString(), false);
 +                client.updateVariable(paintableId, "headerClickCID", cid, true);
 +            }
 +        }
 +
 +        protected void handleCaptionEvent(Event event) {
 +            switch (DOM.eventGetType(event)) {
 +            case Event.ONTOUCHSTART:
 +            case Event.ONMOUSEDOWN:
 +                if (columnReordering
 +                        && Util.isTouchEventOrLeftMouseButton(event)) {
 +                    if (event.getTypeInt() == Event.ONTOUCHSTART) {
 +                        /*
 +                         * prevent using this event in e.g. scrolling
 +                         */
 +                        event.stopPropagation();
 +                    }
 +                    dragging = true;
 +                    moved = false;
 +                    colIndex = getColIndexByKey(cid);
 +                    DOM.setCapture(getElement());
 +                    headerX = tHead.getAbsoluteLeft();
 +                    event.preventDefault(); // prevent selecting text &&
 +                                            // generated touch events
 +                }
 +                break;
 +            case Event.ONMOUSEUP:
 +            case Event.ONTOUCHEND:
 +            case Event.ONTOUCHCANCEL:
 +                if (columnReordering
 +                        && Util.isTouchEventOrLeftMouseButton(event)) {
 +                    dragging = false;
 +                    DOM.releaseCapture(getElement());
 +                    if (moved) {
 +                        hideFloatingCopy();
 +                        tHead.removeSlotFocus();
 +                        if (closestSlot != colIndex
 +                                && closestSlot != (colIndex + 1)) {
 +                            if (closestSlot > colIndex) {
 +                                reOrderColumn(cid, closestSlot - 1);
 +                            } else {
 +                                reOrderColumn(cid, closestSlot);
 +                            }
 +                        }
 +                    }
 +                    if (Util.isTouchEvent(event)) {
 +                        /*
 +                         * Prevent using in e.g. scrolling and prevent generated
 +                         * events.
 +                         */
 +                        event.preventDefault();
 +                        event.stopPropagation();
 +                    }
 +                }
 +
 +                if (!moved) {
 +                    // mouse event was a click to header -> sort column
 +                    if (sortable && Util.isTouchEventOrLeftMouseButton(event)) {
 +                        if (sortColumn.equals(cid)) {
 +                            // just toggle order
 +                            client.updateVariable(paintableId, "sortascending",
 +                                    !sortAscending, false);
 +                        } else {
 +                            // set table sorted by this column
 +                            client.updateVariable(paintableId, "sortcolumn",
 +                                    cid, false);
 +                        }
 +                        // get also cache columns at the same request
 +                        scrollBodyPanel.setScrollPosition(0);
 +                        firstvisible = 0;
 +                        rowRequestHandler.setReqFirstRow(0);
 +                        rowRequestHandler.setReqRows((int) (2 * pageLength
 +                                * cache_rate + pageLength));
 +                        rowRequestHandler.deferRowFetch(); // some validation +
 +                                                           // defer 250ms
 +                        rowRequestHandler.cancel(); // instead of waiting
 +                        rowRequestHandler.run(); // run immediately
 +                    }
 +                    fireHeaderClickedEvent(event);
 +                    if (Util.isTouchEvent(event)) {
 +                        /*
 +                         * Prevent using in e.g. scrolling and prevent generated
 +                         * events.
 +                         */
 +                        event.preventDefault();
 +                        event.stopPropagation();
 +                    }
 +                    break;
 +                }
 +                break;
 +            case Event.ONDBLCLICK:
 +                fireHeaderClickedEvent(event);
 +                break;
 +            case Event.ONTOUCHMOVE:
 +            case Event.ONMOUSEMOVE:
 +                if (dragging && Util.isTouchEventOrLeftMouseButton(event)) {
 +                    if (event.getTypeInt() == Event.ONTOUCHMOVE) {
 +                        /*
 +                         * prevent using this event in e.g. scrolling
 +                         */
 +                        event.stopPropagation();
 +                    }
 +                    if (!moved) {
 +                        createFloatingCopy();
 +                        moved = true;
 +                    }
 +
 +                    final int clientX = Util.getTouchOrMouseClientX(event);
 +                    final int x = clientX + tHead.hTableWrapper.getScrollLeft();
 +                    int slotX = headerX;
 +                    closestSlot = colIndex;
 +                    int closestDistance = -1;
 +                    int start = 0;
 +                    if (showRowHeaders) {
 +                        start++;
 +                    }
 +                    final int visibleCellCount = tHead.getVisibleCellCount();
 +                    for (int i = start; i <= visibleCellCount; i++) {
 +                        if (i > 0) {
 +                            final String colKey = getColKeyByIndex(i - 1);
 +                            slotX += getColWidth(colKey);
 +                        }
 +                        final int dist = Math.abs(x - slotX);
 +                        if (closestDistance == -1 || dist < closestDistance) {
 +                            closestDistance = dist;
 +                            closestSlot = i;
 +                        }
 +                    }
 +                    tHead.focusSlot(closestSlot);
 +
 +                    updateFloatingCopysPosition(clientX, -1);
 +                }
 +                break;
 +            default:
 +                break;
 +            }
 +        }
 +
 +        private void onResizeEvent(Event event) {
 +            switch (DOM.eventGetType(event)) {
 +            case Event.ONMOUSEDOWN:
 +                if (!Util.isTouchEventOrLeftMouseButton(event)) {
 +                    return;
 +                }
 +                isResizing = true;
 +                DOM.setCapture(getElement());
 +                dragStartX = DOM.eventGetClientX(event);
 +                colIndex = getColIndexByKey(cid);
 +                originalWidth = getWidth();
 +                DOM.eventPreventDefault(event);
 +                break;
 +            case Event.ONMOUSEUP:
 +                if (!Util.isTouchEventOrLeftMouseButton(event)) {
 +                    return;
 +                }
 +                isResizing = false;
 +                DOM.releaseCapture(getElement());
 +                tHead.disableAutoColumnWidthCalculation(this);
 +
 +                // Ensure last header cell is taking into account possible
 +                // column selector
 +                HeaderCell lastCell = tHead.getHeaderCell(tHead
 +                        .getVisibleCellCount() - 1);
 +                tHead.resizeCaptionContainer(lastCell);
 +                triggerLazyColumnAdjustment(true);
 +
 +                fireColumnResizeEvent(cid, originalWidth, getColWidth(cid));
 +                break;
 +            case Event.ONMOUSEMOVE:
 +                if (!Util.isTouchEventOrLeftMouseButton(event)) {
 +                    return;
 +                }
 +                if (isResizing) {
 +                    final int deltaX = DOM.eventGetClientX(event) - dragStartX;
 +                    if (deltaX == 0) {
 +                        return;
 +                    }
 +                    tHead.disableAutoColumnWidthCalculation(this);
 +
 +                    int newWidth = originalWidth + deltaX;
 +                    if (newWidth < getMinWidth()) {
 +                        newWidth = getMinWidth();
 +                    }
 +                    setColWidth(colIndex, newWidth, true);
 +                    triggerLazyColumnAdjustment(false);
 +                    forceRealignColumnHeaders();
 +                }
 +                break;
 +            default:
 +                break;
 +            }
 +        }
 +
 +        public int getMinWidth() {
 +            int cellExtraWidth = 0;
 +            if (scrollBody != null) {
 +                cellExtraWidth += scrollBody.getCellExtraWidth();
 +            }
 +            return cellExtraWidth + sortIndicator.getOffsetWidth();
 +        }
 +
 +        public String getCaption() {
 +            return DOM.getInnerText(captionContainer);
 +        }
 +
 +        public boolean isEnabled() {
 +            return getParent() != null;
 +        }
 +
 +        public void setAlign(char c) {
 +            final String ALIGN_PREFIX = CLASSNAME + "-caption-container-align-";
 +            if (align != c) {
 +                captionContainer.removeClassName(ALIGN_PREFIX + "center");
 +                captionContainer.removeClassName(ALIGN_PREFIX + "right");
 +                captionContainer.removeClassName(ALIGN_PREFIX + "left");
 +                switch (c) {
 +                case ALIGN_CENTER:
 +                    captionContainer.addClassName(ALIGN_PREFIX + "center");
 +                    break;
 +                case ALIGN_RIGHT:
 +                    captionContainer.addClassName(ALIGN_PREFIX + "right");
 +                    break;
 +                default:
 +                    captionContainer.addClassName(ALIGN_PREFIX + "left");
 +                    break;
 +                }
 +            }
 +            align = c;
 +        }
 +
 +        public char getAlign() {
 +            return align;
 +        }
 +
 +        /**
 +         * Detects the natural minimum width for the column of this header cell.
 +         * If column is resized by user or the width is defined by server the
 +         * actual width is returned. Else the natural min width is returned.
 +         * 
 +         * @param columnIndex
 +         *            column index hint, if -1 (unknown) it will be detected
 +         * 
 +         * @return
 +         */
 +        public int getNaturalColumnWidth(int columnIndex) {
 +            if (isDefinedWidth()) {
 +                return width;
 +            } else {
 +                if (naturalWidth < 0) {
 +                    // This is recently revealed column. Try to detect a proper
 +                    // value (greater of header and data
 +                    // cols)
 +
 +                    int hw = captionContainer.getOffsetWidth()
 +                            + scrollBody.getCellExtraWidth();
 +                    if (BrowserInfo.get().isGecko()) {
 +                        hw += sortIndicator.getOffsetWidth();
 +                    }
 +                    if (columnIndex < 0) {
 +                        columnIndex = 0;
 +                        for (Iterator<Widget> it = tHead.iterator(); it
 +                                .hasNext(); columnIndex++) {
 +                            if (it.next() == this) {
 +                                break;
 +                            }
 +                        }
 +                    }
 +                    final int cw = scrollBody.getColWidth(columnIndex);
 +                    naturalWidth = (hw > cw ? hw : cw);
 +                }
 +                return naturalWidth;
 +            }
 +        }
 +
 +        public void setExpandRatio(float floatAttribute) {
 +            if (floatAttribute != expandRatio) {
 +                triggerLazyColumnAdjustment(false);
 +            }
 +            expandRatio = floatAttribute;
 +        }
 +
 +        public float getExpandRatio() {
 +            return expandRatio;
 +        }
 +
 +        public boolean isSorted() {
 +            return sorted;
 +        }
 +    }
 +
 +    /**
 +     * HeaderCell that is header cell for row headers.
 +     * 
 +     * Reordering disabled and clicking on it resets sorting.
 +     */
 +    public class RowHeadersHeaderCell extends HeaderCell {
 +
 +        RowHeadersHeaderCell() {
 +            super(ROW_HEADER_COLUMN_KEY, "");
 +            this.setStyleName(CLASSNAME + "-header-cell-rowheader");
 +        }
 +
 +        @Override
 +        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 = 900000;
 +
 +        ArrayList<Widget> visibleCells = new ArrayList<Widget>();
 +
 +        HashMap<String, HeaderCell> availableCells = new HashMap<String, HeaderCell>();
 +
 +        Element div = DOM.createDiv();
 +        Element hTableWrapper = DOM.createDiv();
 +        Element hTableContainer = DOM.createDiv();
 +        Element table = DOM.createTable();
 +        Element headerTableBody = DOM.createTBody();
 +        Element tr = DOM.createTR();
 +
 +        private final Element columnSelector = DOM.createDiv();
 +
 +        private int focusedSlot = -1;
 +
 +        public TableHead() {
 +            if (BrowserInfo.get().isIE()) {
 +                table.setPropertyInt("cellSpacing", 0);
 +            }
 +
 +            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(ROW_HEADER_COLUMN_KEY,
 +                    new RowHeadersHeaderCell());
 +        }
 +
 +        public void resizeCaptionContainer(HeaderCell cell) {
 +            HeaderCell lastcell = getHeaderCell(visibleCells.size() - 1);
 +
 +            // Measure column widths
 +            int columnTotalWidth = 0;
 +            for (Widget w : visibleCells) {
 +                columnTotalWidth += w.getOffsetWidth();
 +            }
 +
 +            if (cell == lastcell
 +                    && columnSelector.getOffsetWidth() > 0
 +                    && columnTotalWidth >= div.getOffsetWidth()
 +                            - columnSelector.getOffsetWidth()
 +                    && !hasVerticalScrollbar()) {
 +                // Ensure column caption is visible when placed under the column
 +                // selector widget by shifting and resizing the caption.
 +                int offset = 0;
 +                int diff = div.getOffsetWidth() - columnTotalWidth;
 +                if (diff < columnSelector.getOffsetWidth() && diff > 0) {
 +                    // If the difference is less than the column selectors width
 +                    // then just offset by the
 +                    // difference
 +                    offset = columnSelector.getOffsetWidth() - diff;
 +                } else {
 +                    // Else offset by the whole column selector
 +                    offset = columnSelector.getOffsetWidth();
 +                }
 +                lastcell.resizeCaptionContainer(offset);
 +            } else {
 +                cell.resizeCaptionContainer(0);
 +            }
 +        }
 +
 +        @Override
 +        public void clear() {
 +            for (String cid : availableCells.keySet()) {
 +                removeCell(cid);
 +            }
 +            availableCells.clear();
 +            availableCells.put(ROW_HEADER_COLUMN_KEY,
 +                    new RowHeadersHeaderCell());
 +        }
 +
 +        public void updateCellsFromUIDL(UIDL uidl) {
 +            Iterator<?> it = uidl.getChildIterator();
 +            HashSet<String> updated = new HashSet<String>();
 +            boolean refreshContentWidths = false;
 +            while (it.hasNext()) {
 +                final UIDL col = (UIDL) it.next();
 +                final String cid = col.getStringAttribute("cid");
 +                updated.add(cid);
 +
 +                String caption = buildCaptionHtmlSnippet(col);
 +                HeaderCell c = getHeaderCell(cid);
 +                if (c == null) {
 +                    c = new HeaderCell(cid, caption);
 +                    availableCells.put(cid, c);
 +                    if (initializedAndAttached) {
 +                        // we will need a column width recalculation
 +                        initializedAndAttached = false;
 +                        initialContentReceived = false;
 +                        isNewBody = true;
 +                    }
 +                } else {
 +                    c.setText(caption);
 +                }
 +
 +                if (col.hasAttribute("sortable")) {
 +                    c.setSortable(true);
 +                    if (cid.equals(sortColumn)) {
 +                        c.setSorted(true);
 +                    } else {
 +                        c.setSorted(false);
 +                    }
 +                } else {
 +                    c.setSortable(false);
 +                }
 +
 +                if (col.hasAttribute("align")) {
 +                    c.setAlign(col.getStringAttribute("align").charAt(0));
 +                } else {
 +                    c.setAlign(ALIGN_LEFT);
 +
 +                }
 +                if (col.hasAttribute("width")) {
 +                    final String widthStr = col.getStringAttribute("width");
 +                    // Make sure to accomodate for the sort indicator if
 +                    // necessary.
 +                    int width = Integer.parseInt(widthStr);
 +                    if (width < c.getMinWidth()) {
 +                        width = c.getMinWidth();
 +                    }
 +                    if (width != c.getWidth() && scrollBody != null) {
 +                        // Do a more thorough update if a column is resized from
 +                        // the server *after* the header has been properly
 +                        // initialized
 +                        final int colIx = getColIndexByKey(c.cid);
 +                        final int newWidth = width;
 +                        Scheduler.get().scheduleDeferred(
 +                                new ScheduledCommand() {
 +                                    public void execute() {
 +                                        setColWidth(colIx, newWidth, true);
 +                                    }
 +                                });
 +                        refreshContentWidths = true;
 +                    } else {
 +                        c.setWidth(width, true);
 +                    }
 +                } else if (recalcWidths) {
 +                    c.setUndefinedWidth();
 +                }
 +                if (col.hasAttribute("er")) {
 +                    c.setExpandRatio(col.getFloatAttribute("er"));
 +                }
 +                if (col.hasAttribute("collapsed")) {
 +                    // ensure header is properly removed from parent (case when
 +                    // collapsing happens via servers side api)
 +                    if (c.isAttached()) {
 +                        c.removeFromParent();
 +                        headerChangedDuringUpdate = true;
 +                    }
 +                }
 +            }
 +
 +            if (refreshContentWidths) {
 +                // Recalculate the column sizings if any column has changed
 +                Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 +                    public void execute() {
 +                        triggerLazyColumnAdjustment(true);
 +                    }
 +                });
 +            }
 +
 +            // check for orphaned header cells
 +            for (Iterator<String> cit = availableCells.keySet().iterator(); cit
 +                    .hasNext();) {
 +                String cid = cit.next();
 +                if (!updated.contains(cid)) {
 +                    removeCell(cid);
 +                    cit.remove();
 +                    // we will need a column width recalculation, since columns
 +                    // with expand ratios should expand to fill the void.
 +                    initializedAndAttached = false;
 +                    initialContentReceived = false;
 +                    isNewBody = true;
 +                }
 +            }
 +        }
 +
 +        public void enableColumn(String cid, int index) {
 +            final HeaderCell c = getHeaderCell(cid);
 +            if (!c.isEnabled() || getHeaderCell(index) != c) {
 +                setHeaderCell(index, c);
 +                if (initializedAndAttached) {
 +                    headerChangedDuringUpdate = true;
 +                }
 +            }
 +        }
 +
 +        public int getVisibleCellCount() {
 +            return visibleCells.size();
 +        }
 +
 +        public void setHorizontalScrollPosition(int scrollLeft) {
 +            hTableWrapper.setScrollLeft(scrollLeft);
 +        }
 +
 +        public void setColumnCollapsingAllowed(boolean cc) {
 +            if (cc) {
 +                columnSelector.getStyle().setDisplay(Display.BLOCK);
 +            } else {
 +                columnSelector.getStyle().setDisplay(Display.NONE);
 +            }
 +        }
 +
 +        public void disableBrowserIntelligence() {
 +            hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX);
 +        }
 +
 +        public void enableBrowserIntelligence() {
 +            hTableContainer.getStyle().clearWidth();
 +        }
 +
 +        public void setHeaderCell(int index, HeaderCell cell) {
 +            if (cell.isEnabled()) {
 +                // we're moving the cell
 +                DOM.removeChild(tr, cell.getElement());
 +                orphan(cell);
 +                visibleCells.remove(cell);
 +            }
 +            if (index < visibleCells.size()) {
 +                // insert to right slot
 +                DOM.insertChild(tr, cell.getElement(), index);
 +                adopt(cell);
 +                visibleCells.add(index, cell);
 +            } 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 >= 0 && 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 availableCells.get(cid);
 +        }
 +
 +        public void moveCell(int oldIndex, int newIndex) {
 +            final HeaderCell hCell = getHeaderCell(oldIndex);
 +            final Element cell = hCell.getElement();
 +
 +            visibleCells.remove(oldIndex);
 +            DOM.removeChild(tr, cell);
 +
 +            DOM.insertChild(tr, cell, newIndex);
 +            visibleCells.add(newIndex, hCell);
 +        }
 +
 +        public Iterator<Widget> iterator() {
 +            return visibleCells.iterator();
 +        }
 +
 +        @Override
 +        public boolean remove(Widget w) {
 +            if (visibleCells.contains(w)) {
 +                visibleCells.remove(w);
 +                orphan(w);
 +                DOM.removeChild(DOM.getParent(w.getElement()), w.getElement());
 +                return true;
 +            }
 +            return false;
 +        }
 +
 +        public void removeCell(String colKey) {
 +            final HeaderCell c = getHeaderCell(colKey);
 +            remove(c);
 +        }
 +
 +        private void focusSlot(int index) {
 +            removeSlotFocus();
 +            if (index > 0) {
 +                DOM.setElementProperty(
 +                        DOM.getFirstChild(DOM.getChild(tr, index - 1)),
 +                        "className", CLASSNAME + "-resizer " + CLASSNAME
 +                                + "-focus-slot-right");
 +            } else {
 +                DOM.setElementProperty(
 +                        DOM.getFirstChild(DOM.getChild(tr, index)),
 +                        "className", CLASSNAME + "-resizer " + CLASSNAME
 +                                + "-focus-slot-left");
 +            }
 +            focusedSlot = index;
 +        }
 +
 +        private void removeSlotFocus() {
 +            if (focusedSlot < 0) {
 +                return;
 +            }
 +            if (focusedSlot == 0) {
 +                DOM.setElementProperty(
 +                        DOM.getFirstChild(DOM.getChild(tr, focusedSlot)),
 +                        "className", CLASSNAME + "-resizer");
 +            } else if (focusedSlot > 0) {
 +                DOM.setElementProperty(
 +                        DOM.getFirstChild(DOM.getChild(tr, focusedSlot - 1)),
 +                        "className", CLASSNAME + "-resizer");
 +            }
 +            focusedSlot = -1;
 +        }
 +
 +        @Override
 +        public void onBrowserEvent(Event event) {
 +            if (enabled) {
 +                if (event.getEventTarget().cast() == columnSelector) {
 +                    final int left = DOM.getAbsoluteLeft(columnSelector);
 +                    final int top = DOM.getAbsoluteTop(columnSelector)
 +                            + DOM.getElementPropertyInt(columnSelector,
 +                                    "offsetHeight");
 +                    client.getContextMenu().showAt(this, left, top);
 +                }
 +            }
 +        }
 +
 +        @Override
 +        protected void onDetach() {
 +            super.onDetach();
 +            if (client != null) {
 +                client.getContextMenu().ensureHidden(this);
 +            }
 +        }
 +
 +        class VisibleColumnAction extends Action {
 +
 +            String colKey;
 +            private boolean collapsed;
 +            private VScrollTableRow currentlyFocusedRow;
 +
 +            public VisibleColumnAction(String colKey) {
 +                super(VScrollTable.TableHead.this);
 +                this.colKey = colKey;
 +                caption = tHead.getHeaderCell(colKey).getCaption();
 +                currentlyFocusedRow = focusedRow;
 +            }
 +
 +            @Override
 +            public void execute() {
 +                client.getContextMenu().hide();
 +                // toggle selected column
 +                if (collapsedColumns.contains(colKey)) {
 +                    collapsedColumns.remove(colKey);
 +                } else {
 +                    tHead.removeCell(colKey);
 +                    collapsedColumns.add(colKey);
 +                    triggerLazyColumnAdjustment(true);
 +                }
 +
 +                // update variable to server
 +                client.updateVariable(paintableId, "collapsedcolumns",
 +                        collapsedColumns.toArray(new String[collapsedColumns
 +                                .size()]), false);
 +                // let rowRequestHandler determine proper rows
 +                rowRequestHandler.refreshContent();
 +                lazyRevertFocusToRow(currentlyFocusedRow);
 +            }
 +
 +            public void setCollapsed(boolean b) {
 +                collapsed = b;
 +            }
 +
 +            /**
 +             * Override default method to distinguish on/off columns
 +             */
 +            @Override
 +            public String getHTML() {
 +                final StringBuffer buf = new StringBuffer();
 +                if (collapsed) {
 +                    buf.append("<span class=\"v-off\">");
 +                } else {
 +                    buf.append("<span class=\"v-on\">");
 +                }
 +                buf.append(super.getHTML());
 +                buf.append("</span>");
 +
 +                return buf.toString();
 +            }
 +
 +        }
 +
 +        /*
 +         * Returns columns as Action array for column select popup
 +         */
 +        public Action[] getActions() {
 +            Object[] cols;
 +            if (columnReordering && columnOrder != null) {
 +                cols = columnOrder;
 +            } else {
 +                // if columnReordering is disabled, we need different way to get
 +                // all available columns
 +                cols = visibleColOrder;
 +                cols = new Object[visibleColOrder.length
 +                        + collapsedColumns.size()];
 +                int i;
 +                for (i = 0; i < visibleColOrder.length; i++) {
 +                    cols[i] = visibleColOrder[i];
 +                }
 +                for (final Iterator<String> it = collapsedColumns.iterator(); it
 +                        .hasNext();) {
 +                    cols[i++] = it.next();
 +                }
 +            }
 +            final Action[] actions = new Action[cols.length];
 +
 +            for (int i = 0; i < cols.length; i++) {
 +                final String cid = (String) cols[i];
 +                final HeaderCell c = getHeaderCell(cid);
 +                final VisibleColumnAction a = new VisibleColumnAction(
 +                        c.getColKey());
 +                a.setCaption(c.getCaption());
 +                if (!c.isEnabled()) {
 +                    a.setCollapsed(true);
 +                }
 +                actions[i] = a;
 +            }
 +            return actions;
 +        }
 +
 +        public ApplicationConnection getClient() {
 +            return client;
 +        }
 +
 +        public String getPaintableId() {
 +            return paintableId;
 +        }
 +
 +        /**
 +         * Returns column alignments for visible columns
 +         */
 +        public char[] getColumnAlignments() {
 +            final Iterator<Widget> it = visibleCells.iterator();
 +            final char[] aligns = new char[visibleCells.size()];
 +            int colIndex = 0;
 +            while (it.hasNext()) {
 +                aligns[colIndex++] = ((HeaderCell) it.next()).getAlign();
 +            }
 +            return aligns;
 +        }
 +
 +        /**
 +         * Disables the automatic calculation of all column widths by forcing
 +         * the widths to be "defined" thus turning off expand ratios and such.
 +         */
 +        public void disableAutoColumnWidthCalculation(HeaderCell source) {
 +            for (HeaderCell cell : availableCells.values()) {
 +                cell.disableAutoWidthCalculation();
 +            }
 +            // fire column resize events for all columns but the source of the
 +            // resize action, since an event will fire separately for this.
 +            ArrayList<HeaderCell> columns = new ArrayList<HeaderCell>(
 +                    availableCells.values());
 +            columns.remove(source);
 +            sendColumnWidthUpdates(columns);
 +            forceRealignColumnHeaders();
 +        }
 +    }
 +
 +    /**
 +     * A cell in the footer
 +     */
 +    public class FooterCell extends Widget {
 +        private final Element td = DOM.createTD();
 +        private final Element captionContainer = DOM.createDiv();
 +        private char align = ALIGN_LEFT;
 +        private int width = -1;
 +        private float expandRatio = 0;
 +        private final String cid;
 +        boolean definedWidth = false;
 +        private int naturalWidth = -1;
 +
 +        public FooterCell(String colId, String headerText) {
 +            cid = colId;
 +
 +            setText(headerText);
 +
 +            DOM.setElementProperty(captionContainer, "className", CLASSNAME
 +                    + "-footer-container");
 +
 +            // ensure no clipping initially (problem on column additions)
 +            DOM.setStyleAttribute(captionContainer, "overflow", "visible");
 +
 +            DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS);
 +
 +            DOM.appendChild(td, captionContainer);
 +
 +            DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
 +                    | Event.ONCONTEXTMENU);
 +
 +            setElement(td);
 +        }
 +
 +        /**
 +         * Sets the text of the footer
 +         * 
 +         * @param footerText
 +         *            The text in the footer
 +         */
 +        public void setText(String footerText) {
 +            DOM.setInnerHTML(captionContainer, footerText);
 +        }
 +
 +        /**
 +         * Set alignment of the text in the cell
 +         * 
 +         * @param c
 +         *            The alignment which can be ALIGN_CENTER, ALIGN_LEFT,
 +         *            ALIGN_RIGHT
 +         */
 +        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;
 +        }
 +
 +        /**
 +         * Get the alignment of the text int the cell
 +         * 
 +         * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT
 +         */
 +        public char getAlign() {
 +            return align;
 +        }
 +
 +        /**
 +         * Sets the width of the cell
 +         * 
 +         * @param w
 +         *            The width of the cell
 +         * @param ensureDefinedWidth
 +         *            Ensures the the given width is not recalculated
 +         */
 +        public void setWidth(int w, boolean ensureDefinedWidth) {
 +
 +            if (ensureDefinedWidth) {
 +                definedWidth = true;
 +                // on column resize expand ratio becomes zero
 +                expandRatio = 0;
 +            }
 +            if (width == w) {
 +                return;
 +            }
 +            if (width == -1) {
 +                // go to default mode, clip content if necessary
 +                DOM.setStyleAttribute(captionContainer, "overflow", "");
 +            }
 +            width = w;
 +            if (w == -1) {
 +                DOM.setStyleAttribute(captionContainer, "width", "");
 +                setWidth("");
 +            } else {
 +
 +                /*
 +                 * Reduce width with one pixel for the right border since the
 +                 * footers does not have any spacers between them.
 +                 */
 +                int borderWidths = 1;
 +
 +                // Set the container width (check for negative value)
 +                if (w - borderWidths >= 0) {
 +                    captionContainer.getStyle().setPropertyPx("width",
 +                            w - borderWidths);
 +                } else {
 +                    captionContainer.getStyle().setPropertyPx("width", 0);
 +                }
 +
 +                /*
 +                 * if we already have tBody, set the header width properly, if
 +                 * not defer it. IE will fail with complex float in table header
 +                 * unless TD width is not explicitly set.
 +                 */
 +                if (scrollBody != null) {
 +                    /*
 +                     * Reduce with one since footer does not have any spacers,
 +                     * instead a 1 pixel border.
 +                     */
 +                    int tdWidth = width + scrollBody.getCellExtraWidth()
 +                            - borderWidths;
 +                    setWidth(tdWidth + "px");
 +                } else {
 +                    Scheduler.get().scheduleDeferred(new Command() {
 +                        public void execute() {
 +                            int borderWidths = 1;
 +                            int tdWidth = width
 +                                    + scrollBody.getCellExtraWidth()
 +                                    - borderWidths;
 +                            setWidth(tdWidth + "px");
 +                        }
 +                    });
 +                }
 +            }
 +        }
 +
 +        /**
 +         * Sets the width to undefined
 +         */
 +        public void setUndefinedWidth() {
 +            setWidth(-1, false);
 +        }
 +
 +        /**
 +         * Detects if width is fixed by developer on server side or resized to
 +         * current width by user.
 +         * 
 +         * @return true if defined, false if "natural" width
 +         */
 +        public boolean isDefinedWidth() {
 +            return definedWidth && width >= 0;
 +        }
 +
 +        /**
 +         * Returns the pixels width of the footer cell
 +         * 
 +         * @return The width in pixels
 +         */
 +        public int getWidth() {
 +            return width;
 +        }
 +
 +        /**
 +         * Sets the expand ratio of the cell
 +         * 
 +         * @param floatAttribute
 +         *            The expand ratio
 +         */
 +        public void setExpandRatio(float floatAttribute) {
 +            expandRatio = floatAttribute;
 +        }
 +
 +        /**
 +         * Returns the expand ration of the cell
 +         * 
 +         * @return The expand ratio
 +         */
 +        public float getExpandRatio() {
 +            return expandRatio;
 +        }
 +
 +        /**
 +         * Is the cell enabled?
 +         * 
 +         * @return True if enabled else False
 +         */
 +        public boolean isEnabled() {
 +            return getParent() != null;
 +        }
 +
 +        /**
 +         * Handle column clicking
 +         */
 +
 +        @Override
 +        public void onBrowserEvent(Event event) {
 +            if (enabled && event != null) {
 +                handleCaptionEvent(event);
 +
 +                if (DOM.eventGetType(event) == Event.ONMOUSEUP) {
 +                    scrollBodyPanel.setFocus(true);
 +                }
 +                boolean stopPropagation = true;
 +                if (event.getTypeInt() == Event.ONCONTEXTMENU
 +                        && !client.hasEventListeners(VScrollTable.this,
 +                                FOOTER_CLICK_EVENT_ID)) {
 +                    // Show browser context menu if a footer click listener is
 +                    // not present
 +                    stopPropagation = false;
 +                }
 +                if (stopPropagation) {
 +                    event.stopPropagation();
 +                    event.preventDefault();
 +                }
 +            }
 +        }
 +
 +        /**
 +         * Handles a event on the captions
 +         * 
 +         * @param event
 +         *            The event to handle
 +         */
 +        protected void handleCaptionEvent(Event event) {
 +            if (event.getTypeInt() == Event.ONMOUSEUP
 +                    || event.getTypeInt() == Event.ONDBLCLICK) {
 +                fireFooterClickedEvent(event);
 +            }
 +        }
 +
 +        /**
 +         * Fires a footer click event after the user has clicked a column footer
 +         * cell
 +         * 
 +         * @param event
 +         *            The click event
 +         */
 +        private void fireFooterClickedEvent(Event event) {
 +            if (client.hasEventListeners(VScrollTable.this,
 +                    FOOTER_CLICK_EVENT_ID)) {
 +                MouseEventDetails details = MouseEventDetailsBuilder
 +                        .buildMouseEventDetails(event);
 +                client.updateVariable(paintableId, "footerClickEvent",
 +                        details.toString(), false);
 +                client.updateVariable(paintableId, "footerClickCID", cid, true);
 +            }
 +        }
 +
 +        /**
 +         * Returns the column key of the column
 +         * 
 +         * @return The column key
 +         */
 +        public String getColKey() {
 +            return cid;
 +        }
 +
 +        /**
 +         * Detects the natural minimum width for the column of this header cell.
 +         * If column is resized by user or the width is defined by server the
 +         * actual width is returned. Else the natural min width is returned.
 +         * 
 +         * @param columnIndex
 +         *            column index hint, if -1 (unknown) it will be detected
 +         * 
 +         * @return
 +         */
 +        public int getNaturalColumnWidth(int columnIndex) {
 +            if (isDefinedWidth()) {
 +                return width;
 +            } else {
 +                if (naturalWidth < 0) {
 +                    // This is recently revealed column. Try to detect a proper
 +                    // value (greater of header and data
 +                    // cols)
 +
 +                    final int hw = ((Element) getElement().getLastChild())
 +                            .getOffsetWidth() + scrollBody.getCellExtraWidth();
 +                    if (columnIndex < 0) {
 +                        columnIndex = 0;
 +                        for (Iterator<Widget> it = tHead.iterator(); it
 +                                .hasNext(); columnIndex++) {
 +                            if (it.next() == this) {
 +                                break;
 +                            }
 +                        }
 +                    }
 +                    final int cw = scrollBody.getColWidth(columnIndex);
 +                    naturalWidth = (hw > cw ? hw : cw);
 +                }
 +                return naturalWidth;
 +            }
 +        }
 +
 +        public void setNaturalMinimumColumnWidth(int w) {
 +            naturalWidth = w;
 +        }
 +    }
 +
 +    /**
 +     * HeaderCell that is header cell for row headers.
 +     * 
 +     * Reordering disabled and clicking on it resets sorting.
 +     */
 +    public class RowHeadersFooterCell extends FooterCell {
 +
 +        RowHeadersFooterCell() {
 +            super(ROW_HEADER_COLUMN_KEY, "");
 +        }
 +
 +        @Override
 +        protected void handleCaptionEvent(Event event) {
 +            // NOP: RowHeaders cannot be reordered
 +            // TODO It'd be nice to reset sorting here
 +        }
 +    }
 +
 +    /**
 +     * The footer of the table which can be seen in the bottom of the Table.
 +     */
 +    public class TableFooter extends Panel {
 +
 +        private static final int WRAPPER_WIDTH = 900000;
 +
 +        ArrayList<Widget> visibleCells = new ArrayList<Widget>();
 +        HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>();
 +
 +        Element div = DOM.createDiv();
 +        Element hTableWrapper = DOM.createDiv();
 +        Element hTableContainer = DOM.createDiv();
 +        Element table = DOM.createTable();
 +        Element headerTableBody = DOM.createTBody();
 +        Element tr = DOM.createTR();
 +
 +        public TableFooter() {
 +
 +            DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden");
 +            DOM.setElementProperty(hTableWrapper, "className", CLASSNAME
 +                    + "-footer");
 +
 +            DOM.appendChild(table, headerTableBody);
 +            DOM.appendChild(headerTableBody, tr);
 +            DOM.appendChild(hTableContainer, table);
 +            DOM.appendChild(hTableWrapper, hTableContainer);
 +            DOM.appendChild(div, hTableWrapper);
 +            setElement(div);
 +
 +            setStyleName(CLASSNAME + "-footer-wrap");
 +
 +            availableCells.put(ROW_HEADER_COLUMN_KEY,
 +                    new RowHeadersFooterCell());
 +        }
 +
 +        @Override
 +        public void clear() {
 +            for (String cid : availableCells.keySet()) {
 +                removeCell(cid);
 +            }
 +            availableCells.clear();
 +            availableCells.put(ROW_HEADER_COLUMN_KEY,
 +                    new RowHeadersFooterCell());
 +        }
 +
 +        /*
 +         * (non-Javadoc)
 +         * 
 +         * @see
 +         * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client
 +         * .ui.Widget)
 +         */
 +        @Override
 +        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;
 +        }
 +
 +        /*
 +         * (non-Javadoc)
 +         * 
 +         * @see com.google.gwt.user.client.ui.HasWidgets#iterator()
 +         */
 +        public Iterator<Widget> iterator() {
 +            return visibleCells.iterator();
 +        }
 +
 +        /**
 +         * Gets a footer cell which represents the given columnId
 +         * 
 +         * @param cid
 +         *            The columnId
 +         * 
 +         * @return The cell
 +         */
 +        public FooterCell getFooterCell(String cid) {
 +            return availableCells.get(cid);
 +        }
 +
 +        /**
 +         * Gets a footer cell by using a column index
 +         * 
 +         * @param index
 +         *            The index of the column
 +         * @return The Cell
 +         */
 +        public FooterCell getFooterCell(int index) {
 +            if (index < visibleCells.size()) {
 +                return (FooterCell) visibleCells.get(index);
 +            } else {
 +                return null;
 +            }
 +        }
 +
 +        /**
 +         * Updates the cells contents when updateUIDL request is received
 +         * 
 +         * @param uidl
 +         *            The UIDL
 +         */
 +        public void updateCellsFromUIDL(UIDL uidl) {
 +            Iterator<?> columnIterator = uidl.getChildIterator();
 +            HashSet<String> updated = new HashSet<String>();
 +            while (columnIterator.hasNext()) {
 +                final UIDL col = (UIDL) columnIterator.next();
 +                final String cid = col.getStringAttribute("cid");
 +                updated.add(cid);
 +
 +                String caption = col.hasAttribute("fcaption") ? col
 +                        .getStringAttribute("fcaption") : "";
 +                FooterCell c = getFooterCell(cid);
 +                if (c == null) {
 +                    c = new FooterCell(cid, caption);
 +                    availableCells.put(cid, c);
 +                    if (initializedAndAttached) {
 +                        // we will need a column width recalculation
 +                        initializedAndAttached = false;
 +                        initialContentReceived = false;
 +                        isNewBody = true;
 +                    }
 +                } else {
 +                    c.setText(caption);
 +                }
 +
 +                if (col.hasAttribute("align")) {
 +                    c.setAlign(col.getStringAttribute("align").charAt(0));
 +                } else {
 +                    c.setAlign(ALIGN_LEFT);
 +
 +                }
 +                if (col.hasAttribute("width")) {
 +                    if (scrollBody == null) {
 +                        // Already updated by setColWidth called from
 +                        // TableHeads.updateCellsFromUIDL in case of a server
 +                        // side resize
 +                        final String width = col.getStringAttribute("width");
 +                        c.setWidth(Integer.parseInt(width), true);
 +                    }
 +                } else if (recalcWidths) {
 +                    c.setUndefinedWidth();
 +                }
 +                if (col.hasAttribute("er")) {
 +                    c.setExpandRatio(col.getFloatAttribute("er"));
 +                }
 +                if (col.hasAttribute("collapsed")) {
 +                    // ensure header is properly removed from parent (case when
 +                    // collapsing happens via servers side api)
 +                    if (c.isAttached()) {
 +                        c.removeFromParent();
 +                        headerChangedDuringUpdate = true;
 +                    }
 +                }
 +            }
 +
 +            // check for orphaned header cells
 +            for (Iterator<String> cit = availableCells.keySet().iterator(); cit
 +                    .hasNext();) {
 +                String cid = cit.next();
 +                if (!updated.contains(cid)) {
 +                    removeCell(cid);
 +                    cit.remove();
 +                }
 +            }
 +        }
 +
 +        /**
 +         * Set a footer cell for a specified column index
 +         * 
 +         * @param index
 +         *            The index
 +         * @param cell
 +         *            The footer cell
 +         */
 +        public void setFooterCell(int index, FooterCell cell) {
 +            if (cell.isEnabled()) {
 +                // we're moving the cell
 +                DOM.removeChild(tr, cell.getElement());
 +                orphan(cell);
 +                visibleCells.remove(cell);
 +            }
 +            if (index < visibleCells.size()) {
 +                // insert to right slot
 +                DOM.insertChild(tr, cell.getElement(), index);
 +                adopt(cell);
 +                visibleCells.add(index, cell);
 +            } 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");
 +            }
 +        }
 +
 +        /**
 +         * Remove a cell by using the columnId
 +         * 
 +         * @param colKey
 +         *            The columnId to remove
 +         */
 +        public void removeCell(String colKey) {
 +            final FooterCell c = getFooterCell(colKey);
 +            remove(c);
 +        }
 +
 +        /**
 +         * Enable a column (Sets the footer cell)
 +         * 
 +         * @param cid
 +         *            The columnId
 +         * @param index
 +         *            The index of the column
 +         */
 +        public void enableColumn(String cid, int index) {
 +            final FooterCell c = getFooterCell(cid);
 +            if (!c.isEnabled() || getFooterCell(index) != c) {
 +                setFooterCell(index, c);
 +                if (initializedAndAttached) {
 +                    headerChangedDuringUpdate = true;
 +                }
 +            }
 +        }
 +
 +        /**
 +         * Disable browser measurement of the table width
 +         */
 +        public void disableBrowserIntelligence() {
 +            DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH
 +                    + "px");
 +        }
 +
 +        /**
 +         * Enable browser measurement of the table width
 +         */
 +        public void enableBrowserIntelligence() {
 +            DOM.setStyleAttribute(hTableContainer, "width", "");
 +        }
 +
 +        /**
 +         * Set the horizontal position in the cell in the footer. This is done
 +         * when a horizontal scrollbar is present.
 +         * 
 +         * @param scrollLeft
 +         *            The value of the leftScroll
 +         */
 +        public void setHorizontalScrollPosition(int scrollLeft) {
 +            hTableWrapper.setScrollLeft(scrollLeft);
 +        }
 +
 +        /**
 +         * Swap cells when the column are dragged
 +         * 
 +         * @param oldIndex
 +         *            The old index of the cell
 +         * @param newIndex
 +         *            The new index of the cell
 +         */
 +        public void moveCell(int oldIndex, int newIndex) {
 +            final FooterCell hCell = getFooterCell(oldIndex);
 +            final Element cell = hCell.getElement();
 +
 +            visibleCells.remove(oldIndex);
 +            DOM.removeChild(tr, cell);
 +
 +            DOM.insertChild(tr, cell, newIndex);
 +            visibleCells.add(newIndex, hCell);
 +        }
 +    }
 +
 +    /**
 +     * This Panel can only contain VScrollTableRow type of widgets. This
 +     * "simulates" very large table, keeping spacers which take room of
 +     * unrendered rows.
 +     * 
 +     */
 +    public class VScrollTableBody extends Panel {
 +
 +        public static final int DEFAULT_ROW_HEIGHT = 24;
 +
 +        private double rowHeight = -1;
 +
 +        private final LinkedList<Widget> renderedRows = new LinkedList<Widget>();
 +
 +        /**
 +         * Due some optimizations row height measuring is deferred and initial
 +         * set of rows is rendered detached. Flag set on when table body has
 +         * been attached in dom and rowheight has been measured.
 +         */
 +        private boolean tBodyMeasurementsDone = false;
 +
 +        Element preSpacer = DOM.createDiv();
 +        Element postSpacer = DOM.createDiv();
 +
 +        Element container = DOM.createDiv();
 +
 +        TableSectionElement tBodyElement = Document.get().createTBodyElement();
 +        Element table = DOM.createTable();
 +
 +        private int firstRendered;
 +        private int lastRendered;
 +
 +        private char[] aligns;
 +
 +        protected VScrollTableBody() {
 +            constructDOM();
 +            setElement(container);
 +        }
 +
 +        public VScrollTableRow getRowByRowIndex(int indexInTable) {
 +            int internalIndex = indexInTable - firstRendered;
 +            if (internalIndex >= 0 && internalIndex < renderedRows.size()) {
 +                return (VScrollTableRow) renderedRows.get(internalIndex);
 +            } else {
 +                return null;
 +            }
 +        }
 +
 +        /**
 +         * @return the height of scrollable body, subpixels ceiled.
 +         */
 +        public int getRequiredHeight() {
 +            return preSpacer.getOffsetHeight() + postSpacer.getOffsetHeight()
 +                    + Util.getRequiredHeight(table);
 +        }
 +
 +        private void constructDOM() {
 +            DOM.setElementProperty(table, "className", CLASSNAME + "-table");
 +            if (BrowserInfo.get().isIE()) {
 +                table.setPropertyInt("cellSpacing", 0);
 +            }
 +            DOM.setElementProperty(preSpacer, "className", CLASSNAME
 +                    + "-row-spacer");
 +            DOM.setElementProperty(postSpacer, "className", CLASSNAME
 +                    + "-row-spacer");
 +
 +            table.appendChild(tBodyElement);
 +            DOM.appendChild(container, preSpacer);
 +            DOM.appendChild(container, table);
 +            DOM.appendChild(container, postSpacer);
++            if (BrowserInfo.get().isTouchDevice()) {
++                NodeList<Node> childNodes = container.getChildNodes();
++                for (int i = 0; i < childNodes.getLength(); i++) {
++                    Element item = (Element) childNodes.getItem(i);
++                    item.getStyle().setProperty("webkitTransform",
++                            "translate3d(0,0,0)");
++                }
++            }
 +
 +        }
 +
 +        public int getAvailableWidth() {
 +            int availW = scrollBodyPanel.getOffsetWidth() - getBorderWidth();
 +            return availW;
 +        }
 +
 +        public void renderInitialRows(UIDL rowData, int firstIndex, int rows) {
 +            firstRendered = firstIndex;
 +            lastRendered = firstIndex + rows - 1;
 +            final Iterator<?> it = rowData.getChildIterator();
 +            aligns = tHead.getColumnAlignments();
 +            while (it.hasNext()) {
 +                final VScrollTableRow row = createRow((UIDL) it.next(), aligns);
 +                addRow(row);
 +            }
 +            if (isAttached()) {
 +                fixSpacers();
 +            }
 +        }
 +
 +        public void renderRows(UIDL rowData, int firstIndex, int rows) {
 +            // FIXME REVIEW
 +            aligns = tHead.getColumnAlignments();
 +            final Iterator<?> it = rowData.getChildIterator();
 +            if (firstIndex == lastRendered + 1) {
 +                while (it.hasNext()) {
 +                    final VScrollTableRow row = prepareRow((UIDL) it.next());
 +                    addRow(row);
 +                    lastRendered++;
 +                }
 +                fixSpacers();
 +            } else if (firstIndex + rows == firstRendered) {
 +                final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
 +                int i = rows;
 +                while (it.hasNext()) {
 +                    i--;
 +                    rowArray[i] = prepareRow((UIDL) it.next());
 +                }
 +                for (i = 0; i < rows; i++) {
 +                    addRowBeforeFirstRendered(rowArray[i]);
 +                    firstRendered--;
 +                }
 +            } else {
 +                // completely new set of rows
 +                while (lastRendered + 1 > firstRendered) {
 +                    unlinkRow(false);
 +                }
 +                final VScrollTableRow row = prepareRow((UIDL) it.next());
 +                firstRendered = firstIndex;
 +                lastRendered = firstIndex - 1;
 +                addRow(row);
 +                lastRendered++;
 +                setContainerHeight();
 +                fixSpacers();
 +                while (it.hasNext()) {
 +                    addRow(prepareRow((UIDL) it.next()));
 +                    lastRendered++;
 +                }
 +                fixSpacers();
 +            }
 +
 +            // this may be a new set of rows due content change,
 +            // ensure we have proper cache rows
 +            ensureCacheFilled();
 +        }
 +
 +        /**
 +         * Ensure we have the correct set of rows on client side, e.g. if the
 +         * content on the server side has changed, or the client scroll position
 +         * has changed since the last request.
 +         */
 +        protected void ensureCacheFilled() {
 +            int reactFirstRow = (int) (firstRowInViewPort - pageLength
 +                    * cache_react_rate);
 +            int reactLastRow = (int) (firstRowInViewPort + pageLength + pageLength
 +                    * cache_react_rate);
 +            if (reactFirstRow < 0) {
 +                reactFirstRow = 0;
 +            }
 +            if (reactLastRow >= totalRows) {
 +                reactLastRow = totalRows - 1;
 +            }
 +            if (lastRendered < reactFirstRow || firstRendered > reactLastRow) {
 +                /*
 +                 * #8040 - scroll position is completely changed since the
 +                 * latest request, so request a new set of rows.
 +                 * 
 +                 * TODO: We should probably check whether the fetched rows match
 +                 * the current scroll position right when they arrive, so as to
 +                 * not waste time rendering a set of rows that will never be
 +                 * visible...
 +                 */
 +                rowRequestHandler.setReqFirstRow(reactFirstRow);
 +                rowRequestHandler.setReqRows(reactLastRow - reactFirstRow + 1);
 +                rowRequestHandler.deferRowFetch(1);
 +            } else if (lastRendered < reactLastRow) {
 +                // get some cache rows below visible area
 +                rowRequestHandler.setReqFirstRow(lastRendered + 1);
 +                rowRequestHandler.setReqRows(reactLastRow - lastRendered);
 +                rowRequestHandler.deferRowFetch(1);
 +            } else if (firstRendered > reactFirstRow) {
 +                /*
 +                 * Branch for fetching cache above visible area.
 +                 * 
 +                 * If cache needed for both before and after visible area, this
 +                 * will be rendered after-cache is received and rendered. So in
 +                 * some rare situations the table may make two cache visits to
 +                 * server.
 +                 */
 +                rowRequestHandler.setReqFirstRow(reactFirstRow);
 +                rowRequestHandler.setReqRows(firstRendered - reactFirstRow);
 +                rowRequestHandler.deferRowFetch(1);
 +            }
 +        }
 +
 +        /**
 +         * Inserts rows as provided in the rowData starting at firstIndex.
 +         * 
 +         * @param rowData
 +         * @param firstIndex
 +         * @param rows
 +         *            the number of rows
 +         * @return a list of the rows added.
 +         */
 +        protected List<VScrollTableRow> insertRows(UIDL rowData,
 +                int firstIndex, int rows) {
 +            aligns = tHead.getColumnAlignments();
 +            final Iterator<?> it = rowData.getChildIterator();
 +            List<VScrollTableRow> insertedRows = new ArrayList<VScrollTableRow>();
 +
 +            if (firstIndex == lastRendered + 1) {
 +                while (it.hasNext()) {
 +                    final VScrollTableRow row = prepareRow((UIDL) it.next());
 +                    addRow(row);
 +                    insertedRows.add(row);
 +                    lastRendered++;
 +                }
 +                fixSpacers();
 +            } else if (firstIndex + rows == firstRendered) {
 +                final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
 +                int i = rows;
 +                while (it.hasNext()) {
 +                    i--;
 +                    rowArray[i] = prepareRow((UIDL) it.next());
 +                }
 +                for (i = 0; i < rows; i++) {
 +                    addRowBeforeFirstRendered(rowArray[i]);
 +                    insertedRows.add(rowArray[i]);
 +                    firstRendered--;
 +                }
 +            } else {
 +                // insert in the middle
 +                int ix = firstIndex;
 +                while (it.hasNext()) {
 +                    VScrollTableRow row = prepareRow((UIDL) it.next());
 +                    insertRowAt(row, ix);
 +                    insertedRows.add(row);
 +                    lastRendered++;
 +                    ix++;
 +                }
 +                fixSpacers();
 +            }
 +            return insertedRows;
 +        }
 +
 +        protected List<VScrollTableRow> insertAndReindexRows(UIDL rowData,
 +                int firstIndex, int rows) {
 +            List<VScrollTableRow> inserted = insertRows(rowData, firstIndex,
 +                    rows);
 +            int actualIxOfFirstRowAfterInserted = firstIndex + rows
 +                    - firstRendered;
 +            for (int ix = actualIxOfFirstRowAfterInserted; ix < renderedRows
 +                    .size(); ix++) {
 +                VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
 +                r.setIndex(r.getIndex() + rows);
 +            }
 +            setContainerHeight();
 +            return inserted;
 +        }
 +
 +        protected void insertRowsDeleteBelow(UIDL rowData, int firstIndex,
 +                int rows) {
 +            unlinkAllRowsStartingAt(firstIndex);
 +            insertRows(rowData, firstIndex, rows);
 +            setContainerHeight();
 +        }
 +
 +        /**
 +         * 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 VScrollTableRow prepareRow(UIDL uidl) {
 +            final VScrollTableRow row = createRow(uidl, aligns);
 +            row.initCellWidths();
 +            return row;
 +        }
 +
 +        protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
 +            if (uidl.hasAttribute("gen_html")) {
 +                // This is a generated row.
 +                return new VScrollTableGeneratedRow(uidl, aligns2);
 +            }
 +            return new VScrollTableRow(uidl, aligns2);
 +        }
 +
 +        private void addRowBeforeFirstRendered(VScrollTableRow row) {
 +            row.setIndex(firstRendered - 1);
 +            if (row.isSelected()) {
 +                row.addStyleName("v-selected");
 +            }
 +            tBodyElement.insertBefore(row.getElement(),
 +                    tBodyElement.getFirstChild());
 +            adopt(row);
 +            renderedRows.add(0, row);
 +        }
 +
 +        private void addRow(VScrollTableRow row) {
 +            row.setIndex(firstRendered + renderedRows.size());
 +            if (row.isSelected()) {
 +                row.addStyleName("v-selected");
 +            }
 +            tBodyElement.appendChild(row.getElement());
 +            adopt(row);
 +            renderedRows.add(row);
 +        }
 +
 +        private void insertRowAt(VScrollTableRow row, int index) {
 +            row.setIndex(index);
 +            if (row.isSelected()) {
 +                row.addStyleName("v-selected");
 +            }
 +            if (index > 0) {
 +                VScrollTableRow sibling = getRowByRowIndex(index - 1);
 +                tBodyElement
 +                        .insertAfter(row.getElement(), sibling.getElement());
 +            } else {
 +                VScrollTableRow sibling = getRowByRowIndex(index);
 +                tBodyElement.insertBefore(row.getElement(),
 +                        sibling.getElement());
 +            }
 +            adopt(row);
 +            int actualIx = index - firstRendered;
 +            renderedRows.add(actualIx, row);
 +        }
 +
 +        public Iterator<Widget> iterator() {
 +            return renderedRows.iterator();
 +        }
 +
 +        /**
 +         * @return false if couldn't remove row
 +         */
 +        protected boolean unlinkRow(boolean fromBeginning) {
 +            if (lastRendered - firstRendered < 0) {
 +                return false;
 +            }
 +            int actualIx;
 +            if (fromBeginning) {
 +                actualIx = 0;
 +                firstRendered++;
 +            } else {
 +                actualIx = renderedRows.size() - 1;
 +                lastRendered--;
 +            }
 +            if (actualIx >= 0) {
 +                unlinkRowAtActualIndex(actualIx);
 +                fixSpacers();
 +                return true;
 +            }
 +            return false;
 +        }
 +
 +        protected void unlinkRows(int firstIndex, int count) {
 +            if (count < 1) {
 +                return;
 +            }
 +            if (firstRendered > firstIndex
 +                    && firstRendered < firstIndex + count) {
 +                firstIndex = firstRendered;
 +            }
 +            int lastIndex = firstIndex + count - 1;
 +            if (lastRendered < lastIndex) {
 +                lastIndex = lastRendered;
 +            }
 +            for (int ix = lastIndex; ix >= firstIndex; ix--) {
 +                unlinkRowAtActualIndex(actualIndex(ix));
 +                lastRendered--;
 +            }
 +            fixSpacers();
 +        }
 +
 +        protected void unlinkAndReindexRows(int firstIndex, int count) {
 +            unlinkRows(firstIndex, count);
 +            int actualFirstIx = firstIndex - firstRendered;
 +            for (int ix = actualFirstIx; ix < renderedRows.size(); ix++) {
 +                VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
 +                r.setIndex(r.getIndex() - count);
 +            }
 +            setContainerHeight();
 +        }
 +
 +        protected void unlinkAllRowsStartingAt(int index) {
 +            if (firstRendered > index) {
 +                index = firstRendered;
 +            }
 +            for (int ix = renderedRows.size() - 1; ix >= index; ix--) {
 +                unlinkRowAtActualIndex(actualIndex(ix));
 +                lastRendered--;
 +            }
 +            fixSpacers();
 +        }
 +
 +        private int actualIndex(int index) {
 +            return index - firstRendered;
 +        }
 +
 +        private void unlinkRowAtActualIndex(int index) {
 +            final VScrollTableRow toBeRemoved = (VScrollTableRow) renderedRows
 +                    .get(index);
 +            // Unregister row tooltip
 +            client.registerTooltip(VScrollTable.this, toBeRemoved.getElement(),
 +                    null);
 +            for (int i = 0; i < toBeRemoved.getElement().getChildCount(); i++) {
 +                // Unregister cell tooltips
 +                Element td = toBeRemoved.getElement().getChild(i).cast();
 +                client.registerTooltip(VScrollTable.this, td, null);
 +            }
 +            tBodyElement.removeChild(toBeRemoved.getElement());
 +            orphan(toBeRemoved);
 +            renderedRows.remove(index);
 +        }
 +
 +        @Override
 +        public boolean remove(Widget w) {
 +            throw new UnsupportedOperationException();
 +        }
 +
 +        /**
 +         * Fix container blocks height according to totalRows to avoid
 +         * "bouncing" when scrolling
 +         */
 +        private void setContainerHeight() {
 +            fixSpacers();
 +            DOM.setStyleAttribute(container, "height",
 +                    measureRowHeightOffset(totalRows) + "px");
 +        }
 +
 +        private void fixSpacers() {
 +            int prepx = measureRowHeightOffset(firstRendered);
 +            if (prepx < 0) {
 +                prepx = 0;
 +            }
 +            preSpacer.getStyle().setPropertyPx("height", prepx);
 +            int postpx = measureRowHeightOffset(totalRows - 1)
 +                    - measureRowHeightOffset(lastRendered);
 +            if (postpx < 0) {
 +                postpx = 0;
 +            }
 +            postSpacer.getStyle().setPropertyPx("height", postpx);
 +        }
 +
 +        public double getRowHeight() {
 +            return getRowHeight(false);
 +        }
 +
 +        public double getRowHeight(boolean forceUpdate) {
 +            if (tBodyMeasurementsDone && !forceUpdate) {
 +                return rowHeight;
 +            } else {
 +
 +                if (tBodyElement.getRows().getLength() > 0) {
 +                    int tableHeight = getTableHeight();
 +                    int rowCount = tBodyElement.getRows().getLength();
 +                    rowHeight = tableHeight / (double) rowCount;
 +                } else {
 +                    if (isAttached()) {
 +                        // measure row height by adding a dummy row
 +                        VScrollTableRow scrollTableRow = new VScrollTableRow();
 +                        tBodyElement.appendChild(scrollTableRow.getElement());
 +                        getRowHeight(forceUpdate);
 +                        tBodyElement.removeChild(scrollTableRow.getElement());
 +                    } else {
 +                        // TODO investigate if this can never happen anymore
 +                        return DEFAULT_ROW_HEIGHT;
 +                    }
 +                }
 +                tBodyMeasurementsDone = true;
 +                return rowHeight;
 +            }
 +        }
 +
 +        public int getTableHeight() {
 +            return table.getOffsetHeight();
 +        }
 +
 +        /**
 +         * Returns the width available for column content.
 +         * 
 +         * @param columnIndex
 +         * @return
 +         */
 +        public int getColWidth(int columnIndex) {
 +            if (tBodyMeasurementsDone) {
 +                if (renderedRows.isEmpty()) {
 +                    // no rows yet rendered
 +                    return 0;
 +                }
 +                for (Widget row : renderedRows) {
 +                    if (!(row instanceof VScrollTableGeneratedRow)) {
 +                        TableRowElement tr = row.getElement().cast();
 +                        Element wrapperdiv = tr.getCells().getItem(columnIndex)
 +                                .getFirstChildElement().cast();
 +                        return wrapperdiv.getOffsetWidth();
 +                    }
 +                }
 +                return 0;
 +            } else {
 +                return 0;
 +            }
 +        }
 +
 +        /**
 +         * Sets the content width of a column.
 +         * 
 +         * Due IE limitation, we must set the width to a wrapper elements inside
 +         * table cells (with overflow hidden, which does not work on td
 +         * elements).
 +         * 
 +         * To get this work properly crossplatform, we will also set the width
 +         * of td.
 +         * 
 +         * @param colIndex
 +         * @param w
 +         */
 +        public void setColWidth(int colIndex, int w) {
 +            for (Widget row : renderedRows) {
 +                ((VScrollTableRow) row).setCellWidth(colIndex, w);
 +            }
 +        }
 +
 +        private int cellExtraWidth = -1;
 +
 +        /**
 +         * Method to return the space used for cell paddings + border.
 +         */
 +        private int getCellExtraWidth() {
 +            if (cellExtraWidth < 0) {
 +                detectExtrawidth();
 +            }
 +            return cellExtraWidth;
 +        }
 +
 +        private void detectExtrawidth() {
 +            NodeList<TableRowElement> rows = tBodyElement.getRows();
 +            if (rows.getLength() == 0) {
 +                /* need to temporary add empty row and detect */
 +                VScrollTableRow scrollTableRow = new VScrollTableRow();
 +                tBodyElement.appendChild(scrollTableRow.getElement());
 +                detectExtrawidth();
 +                tBodyElement.removeChild(scrollTableRow.getElement());
 +            } else {
 +                boolean noCells = false;
 +                TableRowElement item = rows.getItem(0);
 +                TableCellElement firstTD = item.getCells().getItem(0);
 +                if (firstTD == null) {
 +                    // content is currently empty, we need to add a fake cell
 +                    // for measuring
 +                    noCells = true;
 +                    VScrollTableRow next = (VScrollTableRow) iterator().next();
 +                    boolean sorted = tHead.getHeaderCell(0) != null ? tHead
 +                            .getHeaderCell(0).isSorted() : false;
 +                    next.addCell(null, "", ALIGN_LEFT, "", true, sorted);
 +                    firstTD = item.getCells().getItem(0);
 +                }
 +                com.google.gwt.dom.client.Element wrapper = firstTD
 +                        .getFirstChildElement();
 +                cellExtraWidth = firstTD.getOffsetWidth()
 +                        - wrapper.getOffsetWidth();
 +                if (noCells) {
 +                    firstTD.getParentElement().removeChild(firstTD);
 +                }
 +            }
 +        }
 +
 +        private void reLayoutComponents() {
 +            for (Widget w : this) {
 +                VScrollTableRow r = (VScrollTableRow) w;
 +                for (Widget widget : r) {
 +                    client.handleComponentRelativeSize(widget);
 +                }
 +            }
 +        }
 +
 +        public int getLastRendered() {
 +            return lastRendered;
 +        }
 +
 +        public int getFirstRendered() {
 +            return firstRendered;
 +        }
 +
 +        public void moveCol(int oldIndex, int newIndex) {
 +
 +            // loop all rows and move given index to its new place
 +            final Iterator<?> rows = iterator();
 +            while (rows.hasNext()) {
 +                final VScrollTableRow row = (VScrollTableRow) rows.next();
 +
 +                final Element td = DOM.getChild(row.getElement(), oldIndex);
 +                if (td != null) {
 +                    DOM.removeChild(row.getElement(), td);
 +
 +                    DOM.insertChild(row.getElement(), td, newIndex);
 +                }
 +            }
 +
 +        }
 +
 +        /**
 +         * Restore row visibility which is set to "none" when the row is
 +         * rendered (due a performance optimization).
 +         */
 +        private void restoreRowVisibility() {
 +            for (Widget row : renderedRows) {
 +                row.getElement().getStyle().setProperty("visibility", "");
 +            }
 +        }
 +
 +        public class VScrollTableRow extends Panel implements ActionOwner {
 +
-                             contextTouchTimeout.cancel();
-                             contextTouchTimeout
-                                     .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
++            private static final int TOUCHSCROLL_TIMEOUT = 100;
 +            private static final int DRAGMODE_MULTIROW = 2;
 +            protected ArrayList<Widget> childWidgets = new ArrayList<Widget>();
 +            private boolean selected = false;
 +            protected final int rowKey;
 +
 +            private String[] actionKeys = null;
 +            private final TableRowElement rowElement;
 +            private boolean mDown;
 +            private int index;
 +            private Event touchStart;
 +            private static final String ROW_CLASSNAME_EVEN = CLASSNAME + "-row";
 +            private static final String ROW_CLASSNAME_ODD = CLASSNAME
 +                    + "-row-odd";
 +            private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500;
 +            private Timer contextTouchTimeout;
 +            private int touchStartY;
 +            private int touchStartX;
 +
 +            private VScrollTableRow(int rowKey) {
 +                this.rowKey = rowKey;
 +                rowElement = Document.get().createTRElement();
 +                setElement(rowElement);
 +                DOM.sinkEvents(getElement(), Event.MOUSEEVENTS
 +                        | Event.TOUCHEVENTS | Event.ONDBLCLICK
 +                        | Event.ONCONTEXTMENU | VTooltip.TOOLTIP_EVENTS);
 +            }
 +
 +            public VScrollTableRow(UIDL uidl, char[] aligns) {
 +                this(uidl.getIntAttribute("key"));
 +
 +                /*
 +                 * Rendering the rows as hidden improves Firefox and Safari
 +                 * performance drastically.
 +                 */
 +                getElement().getStyle().setProperty("visibility", "hidden");
 +
 +                String rowStyle = uidl.getStringAttribute("rowstyle");
 +                if (rowStyle != null) {
 +                    addStyleName(CLASSNAME + "-row-" + rowStyle);
 +                }
 +
 +                String rowDescription = uidl.getStringAttribute("rowdescr");
 +                if (rowDescription != null && !rowDescription.equals("")) {
 +                    TooltipInfo info = new TooltipInfo(rowDescription);
 +                    client.registerTooltip(VScrollTable.this, rowElement, info);
 +                } else {
 +                    // Remove possibly previously set tooltip
 +                    client.registerTooltip(VScrollTable.this, rowElement, null);
 +                }
 +
 +                tHead.getColumnAlignments();
 +                int col = 0;
 +                int visibleColumnIndex = -1;
 +
 +                // row header
 +                if (showRowHeaders) {
 +                    boolean sorted = tHead.getHeaderCell(col).isSorted();
 +                    addCell(uidl, buildCaptionHtmlSnippet(uidl), aligns[col++],
 +                            "rowheader", true, sorted);
 +                    visibleColumnIndex++;
 +                }
 +
 +                if (uidl.hasAttribute("al")) {
 +                    actionKeys = uidl.getStringArrayAttribute("al");
 +                }
 +
 +                addCellsFromUIDL(uidl, aligns, col, visibleColumnIndex);
 +
 +                if (uidl.hasAttribute("selected") && !isSelected()) {
 +                    toggleSelection();
 +                }
 +            }
 +
 +            /**
 +             * Add a dummy row, used for measurements if Table is empty.
 +             */
 +            public VScrollTableRow() {
 +                this(0);
 +                addStyleName(CLASSNAME + "-row");
 +                addCell(null, "_", 'b', "", true, false);
 +            }
 +
 +            protected void initCellWidths() {
 +                final int cells = tHead.getVisibleCellCount();
 +                for (int i = 0; i < cells; i++) {
 +                    int w = VScrollTable.this.getColWidth(getColKeyByIndex(i));
 +                    if (w < 0) {
 +                        w = 0;
 +                    }
 +                    setCellWidth(i, w);
 +                }
 +            }
 +
 +            protected void setCellWidth(int cellIx, int width) {
 +                final Element cell = DOM.getChild(getElement(), cellIx);
 +                cell.getFirstChildElement().getStyle()
 +                        .setPropertyPx("width", width);
 +                cell.getStyle().setPropertyPx("width", width);
 +            }
 +
 +            protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
 +                    int visibleColumnIndex) {
 +                final Iterator<?> cells = uidl.getChildIterator();
 +                while (cells.hasNext()) {
 +                    final Object cell = cells.next();
 +                    visibleColumnIndex++;
 +
 +                    String columnId = visibleColOrder[visibleColumnIndex];
 +
 +                    String style = "";
 +                    if (uidl.hasAttribute("style-" + columnId)) {
 +                        style = uidl.getStringAttribute("style-" + columnId);
 +                    }
 +
 +                    String description = null;
 +                    if (uidl.hasAttribute("descr-" + columnId)) {
 +                        description = uidl.getStringAttribute("descr-"
 +                                + columnId);
 +                    }
 +
 +                    boolean sorted = tHead.getHeaderCell(col).isSorted();
 +                    if (cell instanceof String) {
 +                        addCell(uidl, cell.toString(), aligns[col++], style,
 +                                isRenderHtmlInCells(), sorted, description);
 +                    } else {
 +                        final ComponentConnector cellContent = client
 +                                .getPaintable((UIDL) cell);
 +
 +                        addCell(uidl, cellContent.getWidget(), aligns[col++],
 +                                style, sorted);
 +                    }
 +                }
 +            }
 +
 +            /**
 +             * Overriding this and returning true causes all text cells to be
 +             * rendered as HTML.
 +             * 
 +             * @return always returns false in the default implementation
 +             */
 +            protected boolean isRenderHtmlInCells() {
 +                return false;
 +            }
 +
 +            /**
 +             * Detects whether row is visible in tables viewport.
 +             * 
 +             * @return
 +             */
 +            public boolean isInViewPort() {
 +                int absoluteTop = getAbsoluteTop();
 +                int scrollPosition = scrollBodyPanel.getScrollPosition();
 +                if (absoluteTop < scrollPosition) {
 +                    return false;
 +                }
 +                int maxVisible = scrollPosition
 +                        + scrollBodyPanel.getOffsetHeight() - getOffsetHeight();
 +                if (absoluteTop > maxVisible) {
 +                    return false;
 +                }
 +                return true;
 +            }
 +
 +            /**
 +             * Makes a check based on indexes whether the row is before the
 +             * compared row.
 +             * 
 +             * @param row1
 +             * @return true if this rows index is smaller than in the row1
 +             */
 +            public boolean isBefore(VScrollTableRow row1) {
 +                return getIndex() < row1.getIndex();
 +            }
 +
 +            /**
 +             * Sets the index of the row in the whole table. Currently used just
 +             * to set even/odd classname
 +             * 
 +             * @param indexInWholeTable
 +             */
 +            private void setIndex(int indexInWholeTable) {
 +                index = indexInWholeTable;
 +                boolean isOdd = indexInWholeTable % 2 == 0;
 +                // Inverted logic to be backwards compatible with earlier 6.4.
 +                // It is very strange because rows 1,3,5 are considered "even"
 +                // and 2,4,6 "odd".
 +                //
 +                // First remove any old styles so that both styles aren't
 +                // applied when indexes are updated.
 +                removeStyleName(ROW_CLASSNAME_ODD);
 +                removeStyleName(ROW_CLASSNAME_EVEN);
 +                if (!isOdd) {
 +                    addStyleName(ROW_CLASSNAME_ODD);
 +                } else {
 +                    addStyleName(ROW_CLASSNAME_EVEN);
 +                }
 +            }
 +
 +            public int getIndex() {
 +                return index;
 +            }
 +
 +            @Override
 +            protected void onDetach() {
 +                super.onDetach();
 +                client.getContextMenu().ensureHidden(this);
 +            }
 +
 +            public String getKey() {
 +                return String.valueOf(rowKey);
 +            }
 +
 +            public void addCell(UIDL rowUidl, String text, char align,
 +                    String style, boolean textIsHTML, boolean sorted) {
 +                addCell(rowUidl, text, align, style, textIsHTML, sorted, null);
 +            }
 +
 +            public void addCell(UIDL rowUidl, String text, char align,
 +                    String style, boolean textIsHTML, boolean sorted,
 +                    String description) {
 +                // String only content is optimized by not using Label widget
 +                final TableCellElement td = DOM.createTD().cast();
 +                initCellWithText(text, align, style, textIsHTML, sorted,
 +                        description, td);
 +            }
 +
 +            protected void initCellWithText(String text, char align,
 +                    String style, boolean textIsHTML, boolean sorted,
 +                    String description, final TableCellElement td) {
 +                final Element container = DOM.createDiv();
 +                String className = CLASSNAME + "-cell-content";
 +                if (style != null && !style.equals("")) {
 +                    className += " " + CLASSNAME + "-cell-content-" + style;
 +                }
 +                if (sorted) {
 +                    className += " " + CLASSNAME + "-cell-content-sorted";
 +                }
 +                td.setClassName(className);
 +                container.setClassName(CLASSNAME + "-cell-wrapper");
 +                if (textIsHTML) {
 +                    container.setInnerHTML(text);
 +                } else {
 +                    container.setInnerText(text);
 +                }
 +                if (align != ALIGN_LEFT) {
 +                    switch (align) {
 +                    case ALIGN_CENTER:
 +                        container.getStyle().setProperty("textAlign", "center");
 +                        break;
 +                    case ALIGN_RIGHT:
 +                    default:
 +                        container.getStyle().setProperty("textAlign", "right");
 +                        break;
 +                    }
 +                }
 +
 +                if (description != null && !description.equals("")) {
 +                    TooltipInfo info = new TooltipInfo(description);
 +                    client.registerTooltip(VScrollTable.this, td, info);
 +                } else {
 +                    // Remove possibly previously set tooltip
 +                    client.registerTooltip(VScrollTable.this, td, null);
 +                }
 +
 +                td.appendChild(container);
 +                getElement().appendChild(td);
 +            }
 +
 +            public void addCell(UIDL rowUidl, Widget w, char align,
 +                    String style, boolean sorted) {
 +                final TableCellElement td = DOM.createTD().cast();
 +                initCellWithWidget(w, align, style, sorted, td);
 +            }
 +
 +            protected void initCellWithWidget(Widget w, char align,
 +                    String style, boolean sorted, final TableCellElement td) {
 +                final Element container = DOM.createDiv();
 +                String className = CLASSNAME + "-cell-content";
 +                if (style != null && !style.equals("")) {
 +                    className += " " + CLASSNAME + "-cell-content-" + style;
 +                }
 +                if (sorted) {
 +                    className += " " + CLASSNAME + "-cell-content-sorted";
 +                }
 +                td.setClassName(className);
 +                container.setClassName(CLASSNAME + "-cell-wrapper");
 +                // TODO most components work with this, but not all (e.g.
 +                // Select)
 +                // Old comment: make widget cells respect align.
 +                // text-align:center for IE, margin: auto for others
 +                if (align != ALIGN_LEFT) {
 +                    switch (align) {
 +                    case ALIGN_CENTER:
 +                        container.getStyle().setProperty("textAlign", "center");
 +                        break;
 +                    case ALIGN_RIGHT:
 +                    default:
 +                        container.getStyle().setProperty("textAlign", "right");
 +                        break;
 +                    }
 +                }
 +                td.appendChild(container);
 +                getElement().appendChild(td);
 +                // ensure widget not attached to another element (possible tBody
 +                // change)
 +                w.removeFromParent();
 +                container.appendChild(w.getElement());
 +                adopt(w);
 +                childWidgets.add(w);
 +            }
 +
 +            public Iterator<Widget> iterator() {
 +                return childWidgets.iterator();
 +            }
 +
 +            @Override
 +            public boolean remove(Widget w) {
 +                if (childWidgets.contains(w)) {
 +                    orphan(w);
 +                    DOM.removeChild(DOM.getParent(w.getElement()),
 +                            w.getElement());
 +                    childWidgets.remove(w);
 +                    return true;
 +                } else {
 +                    return false;
 +                }
 +            }
 +
 +            /**
 +             * If there are registered click listeners, sends a click event and
 +             * returns true. Otherwise, does nothing and returns false.
 +             * 
 +             * @param event
 +             * @param targetTdOrTr
 +             * @param immediate
 +             *            Whether the event is sent immediately
 +             * @return Whether a click event was sent
 +             */
 +            private boolean handleClickEvent(Event event, Element targetTdOrTr,
 +                    boolean immediate) {
 +                if (!client.hasEventListeners(VScrollTable.this,
 +                        ITEM_CLICK_EVENT_ID)) {
 +                    // Don't send an event if nobody is listening
 +                    return false;
 +                }
 +
 +                // This row was clicked
 +                client.updateVariable(paintableId, "clickedKey", "" + rowKey,
 +                        false);
 +
 +                if (getElement() == targetTdOrTr.getParentElement()) {
 +                    // A specific column was clicked
 +                    int childIndex = DOM.getChildIndex(getElement(),
 +                            targetTdOrTr);
 +                    String colKey = null;
 +                    colKey = tHead.getHeaderCell(childIndex).getColKey();
 +                    client.updateVariable(paintableId, "clickedColKey", colKey,
 +                            false);
 +                }
 +
 +                MouseEventDetails details = MouseEventDetailsBuilder
 +                        .buildMouseEventDetails(event);
 +
 +                client.updateVariable(paintableId, "clickEvent",
 +                        details.toString(), immediate);
 +
 +                return true;
 +            }
 +
 +            private void handleTooltips(final Event event, Element target) {
 +                if (target.hasTagName("TD")) {
 +                    // Table cell (td)
 +                    Element container = target.getFirstChildElement().cast();
 +                    Element widget = container.getFirstChildElement().cast();
 +
 +                    boolean containsWidget = false;
 +                    for (Widget w : childWidgets) {
 +                        if (widget == w.getElement()) {
 +                            containsWidget = true;
 +                            break;
 +                        }
 +                    }
 +
 +                    if (!containsWidget) {
 +                        // Only text nodes has tooltips
 +                        if (ConnectorMap.get(client).getWidgetTooltipInfo(
 +                                VScrollTable.this, target) != null) {
 +                            // Cell has description, use it
 +                            client.handleTooltipEvent(event, VScrollTable.this,
 +                                    target);
 +                        } else {
 +                            // Cell might have row description, use row
 +                            // description
 +                            client.handleTooltipEvent(event, VScrollTable.this,
 +                                    target.getParentElement());
 +                        }
 +                    }
 +
 +                } else {
 +                    // Table row (tr)
 +                    client.handleTooltipEvent(event, VScrollTable.this, target);
 +                }
 +            }
 +
 +            /*
 +             * React on click that occur on content cells only
 +             */
 +            @Override
 +            public void onBrowserEvent(final Event event) {
 +                if (enabled) {
 +                    final int type = event.getTypeInt();
 +                    final Element targetTdOrTr = getEventTargetTdOrTr(event);
 +                    if (type == Event.ONCONTEXTMENU) {
 +                        showContextMenu(event);
 +                        if (enabled
 +                                && (actionKeys != null || client
 +                                        .hasEventListeners(VScrollTable.this,
 +                                                ITEM_CLICK_EVENT_ID))) {
 +                            /*
 +                             * Prevent browser context menu only if there are
 +                             * action handlers or item click listeners
 +                             * registered
 +                             */
 +                            event.stopPropagation();
 +                            event.preventDefault();
 +                        }
 +                        return;
 +                    }
 +
 +                    boolean targetCellOrRowFound = targetTdOrTr != null;
 +                    if (targetCellOrRowFound) {
 +                        handleTooltips(event, targetTdOrTr);
 +                    }
 +
 +                    switch (type) {
 +                    case Event.ONDBLCLICK:
 +                        if (targetCellOrRowFound) {
 +                            handleClickEvent(event, targetTdOrTr, true);
 +                        }
 +                        break;
 +                    case Event.ONMOUSEUP:
 +                        if (targetCellOrRowFound) {
 +                            mDown = false;
 +                            /*
 +                             * Queue here, send at the same time as the
 +                             * corresponding value change event - see #7127
 +                             */
 +                            boolean clickEventSent = handleClickEvent(event,
 +                                    targetTdOrTr, false);
 +
 +                            if (event.getButton() == Event.BUTTON_LEFT
 +                                    && isSelectable()) {
 +
 +                                // Ctrl+Shift click
 +                                if ((event.getCtrlKey() || event.getMetaKey())
 +                                        && event.getShiftKey()
 +                                        && isMultiSelectModeDefault()) {
 +                                    toggleShiftSelection(false);
 +                                    setRowFocus(this);
 +
 +                                    // Ctrl click
 +                                } else if ((event.getCtrlKey() || event
 +                                        .getMetaKey())
 +                                        && isMultiSelectModeDefault()) {
 +                                    boolean wasSelected = isSelected();
 +                                    toggleSelection();
 +                                    setRowFocus(this);
 +                                    /*
 +                                     * next possible range select must start on
 +                                     * this row
 +                                     */
 +                                    selectionRangeStart = this;
 +                                    if (wasSelected) {
 +                                        removeRowFromUnsentSelectionRanges(this);
 +                                    }
 +
 +                                } else if ((event.getCtrlKey() || event
 +                                        .getMetaKey()) && isSingleSelectMode()) {
 +                                    // Ctrl (or meta) click (Single selection)
 +                                    if (!isSelected()
 +                                            || (isSelected() && nullSelectionAllowed)) {
 +
 +                                        if (!isSelected()) {
 +                                            deselectAll();
 +                                        }
 +
 +                                        toggleSelection();
 +                                        setRowFocus(this);
 +                                    }
 +
 +                                } else if (event.getShiftKey()
 +                                        && isMultiSelectModeDefault()) {
 +                                    // Shift click
 +                                    toggleShiftSelection(true);
 +
 +                                } else {
 +                                    // click
 +                                    boolean currentlyJustThisRowSelected = selectedRowKeys
 +                                            .size() == 1
 +                                            && selectedRowKeys
 +                                                    .contains(getKey());
 +
 +                                    if (!currentlyJustThisRowSelected) {
 +                                        if (isSingleSelectMode()
 +                                                || isMultiSelectModeDefault()) {
 +                                            /*
 +                                             * For default multi select mode
 +                                             * (ctrl/shift) and for single
 +                                             * select mode we need to clear the
 +                                             * previous selection before
 +                                             * selecting a new one when the user
 +                                             * clicks on a row. Only in
 +                                             * multiselect/simple mode the old
 +                                             * selection should remain after a
 +                                             * normal click.
 +                                             */
 +                                            deselectAll();
 +                                        }
 +                                        toggleSelection();
 +                                    } else if ((isSingleSelectMode() || isMultiSelectModeSimple())
 +                                            && nullSelectionAllowed) {
 +                                        toggleSelection();
 +                                    }/*
 +                                      * else NOP to avoid excessive server
 +                                      * visits (selection is removed with
 +                                      * CTRL/META click)
 +                                      */
 +
 +                                    selectionRangeStart = this;
 +                                    setRowFocus(this);
 +                                }
 +
 +                                // Remove IE text selection hack
 +                                if (BrowserInfo.get().isIE()) {
 +                                    ((Element) event.getEventTarget().cast())
 +                                            .setPropertyJSO("onselectstart",
 +                                                    null);
 +                                }
 +                                // Queue value change
 +                                sendSelectedRows(false);
 +                            }
 +                            /*
 +                             * Send queued click and value change events if any
 +                             * If a click event is sent, send value change with
 +                             * it regardless of the immediate flag, see #7127
 +                             */
 +                            if (immediate || clickEventSent) {
 +                                client.sendPendingVariableChanges();
 +                            }
 +                        }
 +                        break;
 +                    case Event.ONTOUCHEND:
 +                    case Event.ONTOUCHCANCEL:
 +                        if (touchStart != null) {
 +                            /*
 +                             * Touch has not been handled as neither context or
 +                             * drag start, handle it as a click.
 +                             */
 +                            Util.simulateClickFromTouchEvent(touchStart, this);
 +                            touchStart = null;
 +                        }
 +                        if (contextTouchTimeout != null) {
 +                            contextTouchTimeout.cancel();
 +                        }
 +                        break;
 +                    case Event.ONTOUCHMOVE:
 +                        if (isSignificantMove(event)) {
 +                            /*
 +                             * TODO figure out scroll delegate don't eat events
 +                             * if row is selected. Null check for active
 +                             * delegate is as a workaround.
 +                             */
 +                            if (dragmode != 0
 +                                    && touchStart != null
 +                                    && (TouchScrollDelegate
 +                                            .getActiveScrollDelegate() == null)) {
 +                                startRowDrag(touchStart, type, targetTdOrTr);
 +                            }
 +                            if (contextTouchTimeout != null) {
 +                                contextTouchTimeout.cancel();
 +                            }
 +                            /*
 +                             * Avoid clicks and drags by clearing touch start
 +                             * flag.
 +                             */
 +                            touchStart = null;
 +                        }
 +
 +                        break;
 +                    case Event.ONTOUCHSTART:
 +                        touchStart = event;
 +                        Touch touch = event.getChangedTouches().get(0);
 +                        // save position to fields, touches in events are same
 +                        // isntance during the operation.
 +                        touchStartX = touch.getClientX();
 +                        touchStartY = touch.getClientY();
 +                        /*
 +                         * Prevent simulated mouse events.
 +                         */
 +                        touchStart.preventDefault();
 +                        if (dragmode != 0 || actionKeys != null) {
 +                            new Timer() {
 +                                @Override
 +                                public void run() {
 +                                    TouchScrollDelegate activeScrollDelegate = TouchScrollDelegate
 +                                            .getActiveScrollDelegate();
 +                                    /*
 +                                     * If there's a scroll delegate, check if
 +                                     * we're actually scrolling and handle it.
 +                                     * If no delegate, do nothing here and let
 +                                     * the row handle potential drag'n'drop or
 +                                     * context menu.
 +                                     */
 +                                    if (activeScrollDelegate != null) {
 +                                        if (activeScrollDelegate.isMoved()) {
 +                                            /*
 +                                             * Prevent the row from handling
 +                                             * touch move/end events (the
 +                                             * delegate handles those) and from
 +                                             * doing drag'n'drop or opening a
 +                                             * context menu.
 +                                             */
 +                                            touchStart = null;
 +                                        } else {
 +                                            /*
 +                                             * Scrolling hasn't started, so
 +                                             * cancel delegate and let the row
 +                                             * handle potential drag'n'drop or
 +                                             * context menu.
 +                                             */
 +                                            activeScrollDelegate
 +                                                    .stopScrolling();
 +                                        }
 +                                    }
 +                                }
 +                            }.schedule(TOUCHSCROLL_TIMEOUT);
 +
 +                            if (contextTouchTimeout == null
 +                                    && actionKeys != null) {
 +                                contextTouchTimeout = new Timer() {
 +                                    @Override
 +                                    public void run() {
 +                                        if (touchStart != null) {
 +                                            showContextMenu(touchStart);
 +                                            touchStart = null;
 +                                        }
 +                                    }
 +                                };
 +                            }
++                            if (contextTouchTimeout != null) {
++                                contextTouchTimeout.cancel();
++                                contextTouchTimeout
++                                        .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
++                            }
 +                        }
 +                        break;
 +                    case Event.ONMOUSEDOWN:
 +                        if (targetCellOrRowFound) {
 +                            setRowFocus(this);
 +                            ensureFocus();
 +                            if (dragmode != 0
 +                                    && (event.getButton() == NativeEvent.BUTTON_LEFT)) {
 +                                startRowDrag(event, type, targetTdOrTr);
 +
 +                            } else if (event.getCtrlKey()
 +                                    || event.getShiftKey()
 +                                    || event.getMetaKey()
 +                                    && isMultiSelectModeDefault()) {
 +
 +                                // Prevent default text selection in Firefox
 +                                event.preventDefault();
 +
 +                                // Prevent default text selection in IE
 +                                if (BrowserInfo.get().isIE()) {
 +                                    ((Element) event.getEventTarget().cast())
 +                                            .setPropertyJSO(
 +                                                    "onselectstart",
 +                                                    getPreventTextSelectionIEHack());
 +                                }
 +
 +                                event.stopPropagation();
 +                            }
 +                        }
 +                        break;
 +                    case Event.ONMOUSEOUT:
 +                        if (targetCellOrRowFound) {
 +                            mDown = false;
 +                        }
 +                        break;
 +                    default:
 +                        break;
 +                    }
 +                }
 +                super.onBrowserEvent(event);
 +            }
 +
 +            private boolean isSignificantMove(Event event) {
 +                if (touchStart == null) {
 +                    // no touch start
 +                    return false;
 +                }
 +                /*
 +                 * TODO calculate based on real distance instead of separate
 +                 * axis checks
 +                 */
 +                Touch touch = event.getChangedTouches().get(0);
 +                if (Math.abs(touch.getClientX() - touchStartX) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
 +                    return true;
 +                }
 +                if (Math.abs(touch.getClientY() - touchStartY) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
 +                    return true;
 +                }
 +                return false;
 +            }
 +
 +            protected void startRowDrag(Event event, final int type,
 +                    Element targetTdOrTr) {
 +                mDown = true;
 +                VTransferable transferable = new VTransferable();
 +                transferable.setDragSource(ConnectorMap.get(client)
 +                        .getConnector(VScrollTable.this));
 +                transferable.setData("itemId", "" + rowKey);
 +                NodeList<TableCellElement> cells = rowElement.getCells();
 +                for (int i = 0; i < cells.getLength(); i++) {
 +                    if (cells.getItem(i).isOrHasChild(targetTdOrTr)) {
 +                        HeaderCell headerCell = tHead.getHeaderCell(i);
 +                        transferable.setData("propertyId", headerCell.cid);
 +                        break;
 +                    }
 +                }
 +
 +                VDragEvent ev = VDragAndDropManager.get().startDrag(
 +                        transferable, event, true);
 +                if (dragmode == DRAGMODE_MULTIROW && isMultiSelectModeAny()
 +                        && selectedRowKeys.contains("" + rowKey)) {
 +                    ev.createDragImage(
 +                            (Element) scrollBody.tBodyElement.cast(), true);
 +                    Element dragImage = ev.getDragImage();
 +                    int i = 0;
 +                    for (Iterator<Widget> iterator = scrollBody.iterator(); iterator
 +                            .hasNext();) {
 +                        VScrollTableRow next = (VScrollTableRow) iterator
 +                                .next();
 +                        Element child = (Element) dragImage.getChild(i++);
 +                        if (!selectedRowKeys.contains("" + next.rowKey)) {
 +                            child.getStyle().setVisibility(Visibility.HIDDEN);
 +                        }
 +                    }
 +                } else {
 +                    ev.createDragImage(getElement(), true);
 +                }
 +                if (type == Event.ONMOUSEDOWN) {
 +                    event.preventDefault();
 +                }
 +                event.stopPropagation();
 +            }
 +
 +            /**
 +             * Finds the TD that the event interacts with. Returns null if the
 +             * target of the event should not be handled. If the event target is
 +             * the row directly this method returns the TR element instead of
 +             * the TD.
 +             * 
 +             * @param event
 +             * @return TD or TR element that the event targets (the actual event
 +             *         target is this element or a child of it)
 +             */
 +            private Element getEventTargetTdOrTr(Event event) {
 +                final Element eventTarget = event.getEventTarget().cast();
 +                Widget widget = Util.findWidget(eventTarget, null);
 +                final Element thisTrElement = getElement();
 +
 +                if (widget != this) {
 +                    /*
 +                     * This is a workaround to make Labels, read only TextFields
 +                     * and Embedded in a Table clickable (see #2688). It is
 +                     * really not a fix as it does not work with a custom read
 +                     * only components (not extending VLabel/VEmbedded).
 +                     */
 +                    while (widget != null && widget.getParent() != this) {
 +                        widget = widget.getParent();
 +                    }
 +
 +                    if (!(widget instanceof VLabel)
 +                            && !(widget instanceof VEmbedded)
 +                            && !(widget instanceof VTextField && ((VTextField) widget)
 +                                    .isReadOnly())) {
 +                        return null;
 +                    }
 +                }
 +                if (eventTarget == thisTrElement) {
 +                    // This was a click on the TR element
 +                    return thisTrElement;
 +                }
 +
 +                // Iterate upwards until we find the TR element
 +                Element element = eventTarget;
 +                while (element != null
 +                        && element.getParentElement().cast() != thisTrElement) {
 +                    element = element.getParentElement().cast();
 +                }
 +                return element;
 +            }
 +
 +            public void showContextMenu(Event event) {
 +                if (enabled && actionKeys != null) {
 +                    // Show context menu if there are registered action handlers
 +                    int left = Util.getTouchOrMouseClientX(event);
 +                    int top = Util.getTouchOrMouseClientY(event);
 +                    top += Window.getScrollTop();
 +                    left += Window.getScrollLeft();
 +                    contextMenu = new ContextMenuDetails(getKey(), left, top);
 +                    client.getContextMenu().showAt(this, left, top);
 +                }
 +            }
 +
 +            /**
 +             * Has the row been selected?
 +             * 
 +             * @return Returns true if selected, else false
 +             */
 +            public boolean isSelected() {
 +                return selected;
 +            }
 +
 +            /**
 +             * Toggle the selection of the row
 +             */
 +            public void toggleSelection() {
 +                selected = !selected;
 +                selectionChanged = true;
 +                if (selected) {
 +                    selectedRowKeys.add(String.valueOf(rowKey));
 +                    addStyleName("v-selected");
 +                } else {
 +                    removeStyleName("v-selected");
 +                    selectedRowKeys.remove(String.valueOf(rowKey));
 +                }
 +            }
 +
 +            /**
 +             * Is called when a user clicks an item when holding SHIFT key down.
 +             * This will select a new range from the last focused row
 +             * 
 +             * @param deselectPrevious
 +             *            Should the previous selected range be deselected
 +             */
 +            private void toggleShiftSelection(boolean deselectPrevious) {
 +
 +                /*
 +                 * Ensures that we are in multiselect mode and that we have a
 +                 * previous selection which was not a deselection
 +                 */
 +                if (isSingleSelectMode()) {
 +                    // No previous selection found
 +                    deselectAll();
 +                    toggleSelection();
 +                    return;
 +                }
 +
 +                // Set the selectable range
 +                VScrollTableRow endRow = this;
 +                VScrollTableRow startRow = selectionRangeStart;
 +                if (startRow == null) {
 +                    startRow = focusedRow;
 +                    // If start row is null then we have a multipage selection
 +                    // from
 +                    // above
 +                    if (startRow == null) {
 +                        startRow = (VScrollTableRow) scrollBody.iterator()
 +                                .next();
 +                        setRowFocus(endRow);
 +                    }
 +                }
 +                // Deselect previous items if so desired
 +                if (deselectPrevious) {
 +                    deselectAll();
 +                }
 +
 +                // we'll ensure GUI state from top down even though selection
 +                // was the opposite way
 +                if (!startRow.isBefore(endRow)) {
 +                    VScrollTableRow tmp = startRow;
 +                    startRow = endRow;
 +                    endRow = tmp;
 +                }
 +                SelectionRange range = new SelectionRange(startRow, endRow);
 +
 +                for (Widget w : scrollBody) {
 +                    VScrollTableRow row = (VScrollTableRow) w;
 +                    if (range.inRange(row)) {
 +                        if (!row.isSelected()) {
 +                            row.toggleSelection();
 +                        }
 +                        selectedRowKeys.add(row.getKey());
 +                    }
 +                }
 +
 +                // Add range
 +                if (startRow != endRow) {
 +                    selectedRowRanges.add(range);
 +                }
 +            }
 +
 +            /*
 +             * (non-Javadoc)
 +             * 
 +             * @see com.vaadin.terminal.gwt.client.ui.IActionOwner#getActions ()
 +             */
 +            public Action[] getActions() {
 +                if (actionKeys == null) {
 +                    return new Action[] {};
 +                }
 +                final Action[] actions = new Action[actionKeys.length];
 +                for (int i = 0; i < actions.length; i++) {
 +                    final String actionKey = actionKeys[i];
 +                    final TreeAction a = new TreeAction(this,
 +                            String.valueOf(rowKey), actionKey) {
 +                        @Override
 +                        public void execute() {
 +                            super.execute();
 +                            lazyRevertFocusToRow(VScrollTableRow.this);
 +                        }
 +                    };
 +                    a.setCaption(getActionCaption(actionKey));
 +                    a.setIconUrl(getActionIcon(actionKey));
 +                    actions[i] = a;
 +                }
 +                return actions;
 +            }
 +
 +            public ApplicationConnection getClient() {
 +                return client;
 +            }
 +
 +            public String getPaintableId() {
 +                return paintableId;
 +            }
 +
 +            private int getColIndexOf(Widget child) {
 +                com.google.gwt.dom.client.Element widgetCell = child
 +                        .getElement().getParentElement().getParentElement();
 +                NodeList<TableCellElement> cells = rowElement.getCells();
 +                for (int i = 0; i < cells.getLength(); i++) {
 +                    if (cells.getItem(i) == widgetCell) {
 +                        return i;
 +                    }
 +                }
 +                return -1;
 +            }
 +
 +            public Widget getWidgetForPaintable() {
 +                return this;
 +            }
 +        }
 +
 +        protected class VScrollTableGeneratedRow extends VScrollTableRow {
 +
 +            private boolean spanColumns;
 +            private boolean htmlContentAllowed;
 +
 +            public VScrollTableGeneratedRow(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);
 +            }
 +
 +            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);
 +            }
 +
 +            @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);
 +            }
 +        }
 +
 +        /**
 +         * Ensure the component has a focus.
 +         * 
 +         * TODO the current implementation simply always calls focus for the
 +         * component. In case the Table at some point implements focus/blur
 +         * listeners, this method needs to be evolved to conditionally call
 +         * focus only if not currently focused.
 +         */
 +        protected void ensureFocus() {
 +            if (!hasFocus) {
 +                scrollBodyPanel.setFocus(true);
 +            }
 +
 +        }
 +
 +    }
 +
 +    /**
 +     * Deselects all items
 +     */
 +    public void deselectAll() {
 +        for (Widget w : scrollBody) {
 +            VScrollTableRow row = (VScrollTableRow) w;
 +            if (row.isSelected()) {
 +                row.toggleSelection();
 +            }
 +        }
 +        // still ensure all selects are removed from (not necessary rendered)
 +        selectedRowKeys.clear();
 +        selectedRowRanges.clear();
 +        // also notify server that it clears all previous selections (the client
 +        // side does not know about the invisible ones)
 +        instructServerToForgetPreviousSelections();
 +    }
 +
 +    /**
 +     * Used in multiselect mode when the client side knows that all selections
 +     * are in the next request.
 +     */
 +    private void instructServerToForgetPreviousSelections() {
 +        client.updateVariable(paintableId, "clearSelections", true, false);
 +    }
 +
 +    /**
 +     * Determines the pagelength when the table height is fixed.
 +     */
 +    public void updatePageLength() {
 +        // Only update if visible and enabled
 +        if (!isVisible() || !enabled) {
 +            return;
 +        }
 +
 +        if (scrollBody == null) {
 +            return;
 +        }
 +
 +        if (isDynamicHeight()) {
 +            return;
 +        }
 +
 +        int rowHeight = (int) Math.round(scrollBody.getRowHeight());
 +        int bodyH = scrollBodyPanel.getOffsetHeight();
 +        int rowsAtOnce = bodyH / rowHeight;
 +        boolean anotherPartlyVisible = ((bodyH % rowHeight) != 0);
 +        if (anotherPartlyVisible) {
 +            rowsAtOnce++;
 +        }
 +        if (pageLength != rowsAtOnce) {
 +            pageLength = rowsAtOnce;
 +            client.updateVariable(paintableId, "pagelength", pageLength, false);
 +
 +            if (!rendering) {
 +                int currentlyVisible = scrollBody.lastRendered
 +                        - scrollBody.firstRendered;
 +                if (currentlyVisible < pageLength
 +                        && currentlyVisible < totalRows) {
 +                    // shake scrollpanel to fill empty space
 +                    scrollBodyPanel.setScrollPosition(scrollTop + 1);
 +                    scrollBodyPanel.setScrollPosition(scrollTop - 1);
 +                }
 +
 +                sizeNeedsInit = true;
 +            }
 +        }
 +
 +    }
 +
 +    void updateWidth() {
 +        if (!isVisible()) {
 +            /*
 +             * Do not update size when the table is hidden as all column widths
 +             * will be set to zero and they won't be recalculated when the table
 +             * is set visible again (until the size changes again)
 +             */
 +            return;
 +        }
 +
 +        if (!isDynamicWidth()) {
 +            int innerPixels = getOffsetWidth() - getBorderWidth();
 +            if (innerPixels < 0) {
 +                innerPixels = 0;
 +            }
 +            setContentWidth(innerPixels);
 +
 +            // readjust undefined width columns
 +            triggerLazyColumnAdjustment(false);
 +
 +        } else {
 +
 +            sizeNeedsInit = true;
 +
 +            // readjust undefined width columns
 +            triggerLazyColumnAdjustment(false);
 +        }
 +
 +        /*
 +         * setting width may affect wheter the component has scrollbars -> needs
 +         * scrolling or not
 +         */
 +        setProperTabIndex();
 +    }
 +
 +    private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300;
 +
 +    private final Timer lazyAdjustColumnWidths = new Timer() {
 +        /**
 +         * Check for column widths, and available width, to see if we can fix
 +         * column widths "optimally". Doing this lazily to avoid expensive
 +         * calculation when resizing is not yet finished.
 +         */
 +        @Override
 +        public void run() {
 +            if (scrollBody == null) {
 +                // Try again later if we get here before scrollBody has been
 +                // initalized
 +                triggerLazyColumnAdjustment(false);
 +                return;
 +            }
 +
 +            Iterator<Widget> headCells = tHead.iterator();
 +            int usedMinimumWidth = 0;
 +            int totalExplicitColumnsWidths = 0;
 +            float expandRatioDivider = 0;
 +            int colIndex = 0;
 +            while (headCells.hasNext()) {
 +                final HeaderCell hCell = (HeaderCell) headCells.next();
 +                if (hCell.isDefinedWidth()) {
 +                    totalExplicitColumnsWidths += hCell.getWidth();
 +                    usedMinimumWidth += hCell.getWidth();
 +                } else {
 +                    usedMinimumWidth += hCell.getNaturalColumnWidth(colIndex);
 +                    expandRatioDivider += hCell.getExpandRatio();
 +                }
 +                colIndex++;
 +            }
 +
 +            int availW = scrollBody.getAvailableWidth();
 +            // Hey IE, are you really sure about this?
 +            availW = scrollBody.getAvailableWidth();
 +            int visibleCellCount = tHead.getVisibleCellCount();
 +            availW -= scrollBody.getCellExtraWidth() * visibleCellCount;
 +            if (willHaveScrollbars()) {
 +                availW -= Util.getNativeScrollbarSize();
 +            }
 +
 +            int extraSpace = availW - usedMinimumWidth;
 +            if (extraSpace < 0) {
 +                extraSpace = 0;
 +            }
 +
 +            int totalUndefinedNaturalWidths = usedMinimumWidth
 +                    - totalExplicitColumnsWidths;
 +
 +            // we have some space that can be divided optimally
 +            HeaderCell hCell;
 +            colIndex = 0;
 +            headCells = tHead.iterator();
 +            int checksum = 0;
 +            while (headCells.hasNext()) {
 +                hCell = (HeaderCell) headCells.next();
 +                if (!hCell.isDefinedWidth()) {
 +                    int w = hCell.getNaturalColumnWidth(colIndex);
 +                    int newSpace;
 +                    if (expandRatioDivider > 0) {
 +                        // divide excess space by expand ratios
 +                        newSpace = Math.round((w + extraSpace
 +                                * hCell.getExpandRatio() / expandRatioDivider));
 +                    } else {
 +                        if (totalUndefinedNaturalWidths != 0) {
 +                            // divide relatively to natural column widths
 +                            newSpace = Math.round(w + (float) extraSpace
 +                                    * (float) w / totalUndefinedNaturalWidths);
 +                        } else {
 +                            newSpace = w;
 +                        }
 +                    }
 +                    checksum += newSpace;
 +                    setColWidth(colIndex, newSpace, false);
 +                } else {
 +                    checksum += hCell.getWidth();
 +                }
 +                colIndex++;
 +            }
 +
 +            if (extraSpace > 0 && checksum != availW) {
 +                /*
 +                 * There might be in some cases a rounding error of 1px when
 +                 * extra space is divided so if there is one then we give the
 +                 * first undefined column 1 more pixel
 +                 */
 +                headCells = tHead.iterator();
 +                colIndex = 0;
 +                while (headCells.hasNext()) {
 +                    HeaderCell hc = (HeaderCell) headCells.next();
 +                    if (!hc.isDefinedWidth()) {
 +                        setColWidth(colIndex,
 +                                hc.getWidth() + availW - checksum, false);
 +                        break;
 +                    }
 +                    colIndex++;
 +                }
 +            }
 +
 +            if (isDynamicHeight() && totalRows == pageLength) {
 +                // fix body height (may vary if lazy loading is offhorizontal
 +                // scrollbar appears/disappears)
 +                int bodyHeight = scrollBody.getRequiredHeight();
 +                boolean needsSpaceForHorizontalScrollbar = (availW < usedMinimumWidth);
 +                if (needsSpaceForHorizontalScrollbar) {
 +                    bodyHeight += Util.getNativeScrollbarSize();
 +                }
 +                int heightBefore = getOffsetHeight();
 +                scrollBodyPanel.setHeight(bodyHeight + "px");
 +                if (heightBefore != getOffsetHeight()) {
 +                    Util.notifyParentOfSizeChange(VScrollTable.this, false);
 +                }
 +            }
 +            scrollBody.reLayoutComponents();
 +            Scheduler.get().scheduleDeferred(new Command() {
 +                public void execute() {
 +                    Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
 +                }
 +            });
 +
 +            forceRealignColumnHeaders();
 +        }
 +
 +    };
 +
 +    private void forceRealignColumnHeaders() {
 +        if (BrowserInfo.get().isIE()) {
 +            /*
 +             * IE does not fire onscroll event if scroll position is reverted to
 +             * 0 due to the content element size growth. Ensure headers are in
 +             * sync with content manually. Safe to use null event as we don't
 +             * actually use the event object in listener.
 +             */
 +            onScroll(null);
 +        }
 +    }
 +
 +    /**
 +     * helper to set pixel size of head and body part
 +     * 
 +     * @param pixels
 +     */
 +    private void setContentWidth(int pixels) {
 +        tHead.setWidth(pixels + "px");
 +        scrollBodyPanel.setWidth(pixels + "px");
 +        tFoot.setWidth(pixels + "px");
 +    }
 +
 +    private int borderWidth = -1;
 +
 +    /**
 +     * @return border left + border right
 +     */
 +    private int getBorderWidth() {
 +        if (borderWidth < 0) {
 +            borderWidth = Util.measureHorizontalPaddingAndBorder(
 +                    scrollBodyPanel.getElement(), 2);
 +            if (borderWidth < 0) {
 +                borderWidth = 0;
 +            }
 +        }
 +        return borderWidth;
 +    }
 +
 +    /**
 +     * Ensures scrollable area is properly sized. This method is used when fixed
 +     * size is used.
 +     */
 +    private int containerHeight;
 +
 +    private void setContainerHeight() {
 +        if (!isDynamicHeight()) {
 +            containerHeight = getOffsetHeight();
 +            containerHeight -= showColHeaders ? tHead.getOffsetHeight() : 0;
 +            containerHeight -= tFoot.getOffsetHeight();
 +            containerHeight -= getContentAreaBorderHeight();
 +            if (containerHeight < 0) {
 +                containerHeight = 0;
 +            }
 +            scrollBodyPanel.setHeight(containerHeight + "px");
 +        }
 +    }
 +
 +    private int contentAreaBorderHeight = -1;
 +    private int scrollLeft;
 +    private int scrollTop;
 +    VScrollTableDropHandler dropHandler;
 +    private boolean navKeyDown;
 +    boolean multiselectPending;
 +
 +    /**
 +     * @return border top + border bottom of the scrollable area of table
 +     */
 +    private int getContentAreaBorderHeight() {
 +        if (contentAreaBorderHeight < 0) {
 +
 +            DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow",
 +                    "hidden");
 +            int oh = scrollBodyPanel.getOffsetHeight();
 +            int ch = scrollBodyPanel.getElement()
 +                    .getPropertyInt("clientHeight");
 +            contentAreaBorderHeight = oh - ch;
 +            DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow",
 +                    "auto");
 +        }
 +        return contentAreaBorderHeight;
 +    }
 +
 +    @Override
 +    public void setHeight(String height) {
 +        if (height.length() == 0
 +                && getElement().getStyle().getHeight().length() != 0) {
 +            /*
 +             * Changing from defined to undefined size -> should do a size init
 +             * to take page length into account again
 +             */
 +            sizeNeedsInit = true;
 +        }
 +        super.setHeight(height);
 +    }
 +
 +    void updateHeight() {
 +        setContainerHeight();
 +
 +        updatePageLength();
 +
 +        if (!rendering) {
 +            // Webkit may sometimes get an odd rendering bug (white space
 +            // between header and body), see bug #3875. Running
 +            // overflow hack here to shake body element a bit.
 +            Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
 +        }
 +
 +        /*
 +         * setting height may affect wheter the component has scrollbars ->
 +         * needs scrolling or not
 +         */
 +        setProperTabIndex();
 +
 +    }
 +
 +    /*
 +     * Overridden due Table might not survive of visibility change (scroll pos
 +     * lost). Example ITabPanel just set contained components invisible and back
 +     * when changing tabs.
 +     */
 +    @Override
 +    public void setVisible(boolean visible) {
 +        if (isVisible() != visible) {
 +            super.setVisible(visible);
 +            if (initializedAndAttached) {
 +                if (visible) {
 +                    Scheduler.get().scheduleDeferred(new Command() {
 +                        public void execute() {
 +                            scrollBodyPanel
 +                                    .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
 +                        }
 +                    });
 +                }
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Helper function to build html snippet for column or row headers
 +     * 
 +     * @param uidl
 +     *            possibly with values caption and icon
 +     * @return html snippet containing possibly an icon + caption text
 +     */
 +    protected String buildCaptionHtmlSnippet(UIDL uidl) {
 +        String s = uidl.hasAttribute("caption") ? uidl
 +                .getStringAttribute("caption") : "";
 +        if (uidl.hasAttribute("icon")) {
 +            s = "<img src=\""
 +                    + Util.escapeAttribute(client.translateVaadinUri(uidl
 +                            .getStringAttribute("icon")))
 +                    + "\" alt=\"icon\" class=\"v-icon\">" + s;
 +        }
 +        return s;
 +    }
 +
 +    /**
 +     * This method has logic which rows needs to be requested from server when
 +     * user scrolls
 +     */
 +    public void onScroll(ScrollEvent event) {
 +        scrollLeft = scrollBodyPanel.getElement().getScrollLeft();
 +        scrollTop = scrollBodyPanel.getScrollPosition();
 +        /*
 +         * #6970 - IE sometimes fires scroll events for a detached table.
 +         * 
 +         * FIXME initializedAndAttached should probably be renamed - its name
 +         * doesn't seem to reflect its semantics. onDetach() doesn't set it to
 +         * false, and changing that might break something else, so we need to
 +         * check isAttached() separately.
 +         */
 +        if (!initializedAndAttached || !isAttached()) {
 +            return;
 +        }
 +        if (!enabled) {
 +            scrollBodyPanel
 +                    .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
 +            return;
 +        }
 +
 +        rowRequestHandler.cancel();
 +
 +        if (BrowserInfo.get().isSafari() && event != null && scrollTop == 0) {
 +            // due to the webkitoverflowworkaround, top may sometimes report 0
 +            // for webkit, although it really is not. Expecting to have the
 +            // correct
 +            // value available soon.
 +            Scheduler.get().scheduleDeferred(new Command() {
 +                public void execute() {
 +                    onScroll(null);
 +                }
 +            });
 +            return;
 +        }
 +
 +        // fix headers horizontal scrolling
 +        tHead.setHorizontalScrollPosition(scrollLeft);
 +
 +        // fix footers horizontal scrolling
 +        tFoot.setHorizontalScrollPosition(scrollLeft);
 +
 +        firstRowInViewPort = calcFirstRowInViewPort();
 +        if (firstRowInViewPort > totalRows - pageLength) {
 +            firstRowInViewPort = totalRows - pageLength;
 +        }
 +
 +        int postLimit = (int) (firstRowInViewPort + (pageLength - 1) + pageLength
 +                * cache_react_rate);
 +        if (postLimit > totalRows - 1) {
 +            postLimit = totalRows - 1;
 +        }
 +        int preLimit = (int) (firstRowInViewPort - pageLength
 +                * cache_react_rate);
 +        if (preLimit < 0) {
 +            preLimit = 0;
 +        }
 +        final int lastRendered = scrollBody.getLastRendered();
 +        final int firstRendered = scrollBody.getFirstRendered();
 +
 +        if (postLimit <= lastRendered && preLimit >= firstRendered) {
 +            // we're within no-react area, no need to request more rows
 +            // remember which firstvisible we requested, in case the server has
 +            // a differing opinion
 +            lastRequestedFirstvisible = firstRowInViewPort;
 +            client.updateVariable(paintableId, "firstvisible",
 +                    firstRowInViewPort, false);
 +            return;
 +        }
 +
 +        if (firstRowInViewPort - pageLength * cache_rate > lastRendered
 +                || firstRowInViewPort + pageLength + pageLength * cache_rate < firstRendered) {
 +            // need a totally new set of rows
 +            rowRequestHandler
 +                    .setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate)));
 +            int last = firstRowInViewPort + (int) (cache_rate * pageLength)
 +                    + pageLength - 1;
 +            if (last >= totalRows) {
 +                last = totalRows - 1;
 +            }
 +            rowRequestHandler.setReqRows(last
 +                    - rowRequestHandler.getReqFirstRow() + 1);
 +            rowRequestHandler.deferRowFetch();
 +            return;
 +        }
 +        if (preLimit < firstRendered) {
 +            // 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
 +            rowRequestHandler.setReqFirstRow(lastRendered + 1);
 +            rowRequestHandler.setReqRows((int) ((firstRowInViewPort
 +                    + pageLength + pageLength * cache_rate) - lastRendered));
 +            rowRequestHandler.deferRowFetch();
 +        }
 +    }
 +
 +    protected int calcFirstRowInViewPort() {
 +        return (int) Math.ceil(scrollTop / scrollBody.getRowHeight());
 +    }
 +
 +    public VScrollTableDropHandler getDropHandler() {
 +        return dropHandler;
 +    }
 +
 +    private static class TableDDDetails {
 +        int overkey = -1;
 +        VerticalDropLocation dropLocation;
 +        String colkey;
 +
 +        @Override
 +        public boolean equals(Object obj) {
 +            if (obj instanceof TableDDDetails) {
 +                TableDDDetails other = (TableDDDetails) obj;
 +                return dropLocation == other.dropLocation
 +                        && overkey == other.overkey
 +                        && ((colkey != null && colkey.equals(other.colkey)) || (colkey == null && other.colkey == null));
 +            }
 +            return false;
 +        }
 +
 +        // @Override
 +        // public int hashCode() {
 +        // return overkey;
 +        // }
 +    }
 +
 +    public class VScrollTableDropHandler extends VAbstractDropHandler {
 +
 +        private static final String ROWSTYLEBASE = "v-table-row-drag-";
 +        private TableDDDetails dropDetails;
 +        private TableDDDetails lastEmphasized;
 +
 +        @Override
 +        public void dragEnter(VDragEvent drag) {
 +            updateDropDetails(drag);
 +            super.dragEnter(drag);
 +        }
 +
 +        private void updateDropDetails(VDragEvent drag) {
 +            dropDetails = new TableDDDetails();
 +            Element elementOver = drag.getElementOver();
 +
 +            VScrollTableRow row = Util.findWidget(elementOver, getRowClass());
 +            if (row != null) {
 +                dropDetails.overkey = row.rowKey;
 +                Element tr = row.getElement();
 +                Element element = elementOver;
 +                while (element != null && element.getParentElement() != tr) {
 +                    element = (Element) element.getParentElement();
 +                }
 +                int childIndex = DOM.getChildIndex(tr, element);
 +                dropDetails.colkey = tHead.getHeaderCell(childIndex)
 +                        .getColKey();
 +                dropDetails.dropLocation = DDUtil.getVerticalDropLocation(
 +                        row.getElement(), drag.getCurrentGwtEvent(), 0.2);
 +            }
 +
 +            drag.getDropDetails().put("itemIdOver", dropDetails.overkey + "");
 +            drag.getDropDetails().put(
 +                    "detail",
 +                    dropDetails.dropLocation != null ? dropDetails.dropLocation
 +                            .toString() : null);
 +
 +        }
 +
 +        private Class<? extends Widget> getRowClass() {
 +            // get the row type this way to make dd work in derived
 +            // implementations
 +            return scrollBody.iterator().next().getClass();
 +        }
 +
 +        @Override
 +        public void dragOver(VDragEvent drag) {
 +            TableDDDetails oldDetails = dropDetails;
 +            updateDropDetails(drag);
 +            if (!oldDetails.equals(dropDetails)) {
 +                deEmphasis();
 +                final TableDDDetails newDetails = dropDetails;
 +                VAcceptCallback cb = new VAcceptCallback() {
 +                    public void accepted(VDragEvent event) {
 +                        if (newDetails.equals(dropDetails)) {
 +                            dragAccepted(event);
 +                        }
 +                        /*
 +                         * Else new target slot already defined, ignore
 +                         */
 +                    }
 +                };
 +                validate(cb, drag);
 +            }
 +        }
 +
 +        @Override
 +        public void dragLeave(VDragEvent drag) {
 +            deEmphasis();
 +            super.dragLeave(drag);
 +        }
 +
 +        @Override
 +        public boolean drop(VDragEvent drag) {
 +            deEmphasis();
 +            return super.drop(drag);
 +        }
 +
 +        private void deEmphasis() {
 +            UIObject.setStyleName(getElement(), CLASSNAME + "-drag", false);
 +            if (lastEmphasized == null) {
 +                return;
 +            }
 +            for (Widget w : scrollBody.renderedRows) {
 +                VScrollTableRow row = (VScrollTableRow) w;
 +                if (lastEmphasized != null
 +                        && row.rowKey == lastEmphasized.overkey) {
 +                    String stylename = ROWSTYLEBASE
 +                            + lastEmphasized.dropLocation.toString()
 +                                    .toLowerCase();
 +                    VScrollTableRow.setStyleName(row.getElement(), stylename,
 +                            false);
 +                    lastEmphasized = null;
 +                    return;
 +                }
 +            }
 +        }
 +
 +        /**
 +         * TODO needs different drop modes ?? (on cells, on rows), now only
 +         * supports rows
 +         */
 +        private void emphasis(TableDDDetails details) {
 +            deEmphasis();
 +            UIObject.setStyleName(getElement(), CLASSNAME + "-drag", true);
 +            // iterate old and new emphasized row
 +            for (Widget w : scrollBody.renderedRows) {
 +                VScrollTableRow row = (VScrollTableRow) w;
 +                if (details != null && details.overkey == row.rowKey) {
 +                    String stylename = ROWSTYLEBASE
 +                            + details.dropLocation.toString().toLowerCase();
 +                    VScrollTableRow.setStyleName(row.getElement(), stylename,
 +                            true);
 +                    lastEmphasized = details;
 +                    return;
 +                }
 +            }
 +        }
 +
 +        @Override
 +        protected void dragAccepted(VDragEvent drag) {
 +            emphasis(dropDetails);
 +        }
 +
 +        @Override
 +        public ComponentConnector getConnector() {
 +            return ConnectorMap.get(client).getConnector(VScrollTable.this);
 +        }
 +
 +        public ApplicationConnection getApplicationConnection() {
 +            return client;
 +        }
 +
 +    }
 +
 +    protected VScrollTableRow getFocusedRow() {
 +        return focusedRow;
 +    }
 +
 +    /**
 +     * Moves the selection head to a specific row
 +     * 
 +     * @param row
 +     *            The row to where the selection head should move
 +     * @return Returns true if focus was moved successfully, else false
 +     */
 +    public boolean setRowFocus(VScrollTableRow row) {
 +
 +        if (!isSelectable()) {
 +            return false;
 +        }
 +
 +        // Remove previous selection
 +        if (focusedRow != null && focusedRow != row) {
 +            focusedRow.removeStyleName(CLASSNAME_SELECTION_FOCUS);
 +        }
 +
 +        if (row != null) {
 +
 +            // Apply focus style to new selection
 +            row.addStyleName(CLASSNAME_SELECTION_FOCUS);
 +
 +            /*
 +             * Trying to set focus on already focused row
 +             */
 +            if (row == focusedRow) {
 +                return false;
 +            }
 +
 +            // Set new focused row
 +            focusedRow = row;
 +
 +            ensureRowIsVisible(row);
 +
 +            return true;
 +        }
 +
 +        return false;
 +    }
 +
 +    /**
 +     * Ensures that the row is visible
 +     * 
 +     * @param row
 +     *            The row to ensure is visible
 +     */
 +    private void ensureRowIsVisible(VScrollTableRow row) {
++        if (BrowserInfo.get().isTouchDevice()) {
++            // Skip due to android devices that have broken scrolltop will may
++            // get odd scrolling here.
++            return;
++        }
 +        Util.scrollIntoViewVertically(row.getElement());
 +    }
 +
 +    /**
 +     * Handles the keyboard events handled by the table
 +     * 
 +     * @param event
 +     *            The keyboard event received
 +     * @return true iff the navigation event was handled
 +     */
 +    protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
 +        if (keycode == KeyCodes.KEY_TAB || keycode == KeyCodes.KEY_SHIFT) {
 +            // Do not handle tab key
 +            return false;
 +        }
 +
 +        // Down navigation
 +        if (!isSelectable() && keycode == getNavigationDownKey()) {
 +            scrollBodyPanel.setScrollPosition(scrollBodyPanel
 +                    .getScrollPosition() + scrollingVelocity);
 +            return true;
 +        } else if (keycode == getNavigationDownKey()) {
 +            if (isMultiSelectModeAny() && moveFocusDown()) {
 +                selectFocusedRow(ctrl, shift);
 +
 +            } else if (isSingleSelectMode() && !shift && moveFocusDown()) {
 +                selectFocusedRow(ctrl, shift);
 +            }
 +            return true;
 +        }
 +
 +        // Up navigation
 +        if (!isSelectable() && keycode == getNavigationUpKey()) {
 +            scrollBodyPanel.setScrollPosition(scrollBodyPanel
 +                    .getScrollPosition() - scrollingVelocity);
 +            return true;
 +        } else if (keycode == getNavigationUpKey()) {
 +            if (isMultiSelectModeAny() && moveFocusUp()) {
 +                selectFocusedRow(ctrl, shift);
 +            } else if (isSingleSelectMode() && !shift && moveFocusUp()) {
 +                selectFocusedRow(ctrl, shift);
 +            }
 +            return true;
 +        }
 +
 +        if (keycode == getNavigationLeftKey()) {
 +            // Left navigation
 +            scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel
 +                    .getHorizontalScrollPosition() - scrollingVelocity);
 +            return true;
 +
 +        } else if (keycode == getNavigationRightKey()) {
 +            // Right navigation
 +            scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel
 +                    .getHorizontalScrollPosition() + scrollingVelocity);
 +            return true;
 +        }
 +
 +        // Select navigation
 +        if (isSelectable() && keycode == getNavigationSelectKey()) {
 +            if (isSingleSelectMode()) {
 +                boolean wasSelected = focusedRow.isSelected();
 +                deselectAll();
 +                if (!wasSelected || !nullSelectionAllowed) {
 +                    focusedRow.toggleSelection();
 +                }
 +            } else {
 +                focusedRow.toggleSelection();
 +                removeRowFromUnsentSelectionRanges(focusedRow);
 +            }
 +
 +            sendSelectedRows();
 +            return true;
 +        }
 +
 +        // Page Down navigation
 +        if (keycode == getNavigationPageDownKey()) {
 +            if (isSelectable()) {
 +                /*
 +                 * If selectable we plagiate MSW behaviour: first scroll to the
 +                 * end of current view. If at the end, scroll down one page
 +                 * length and keep the selected row in the bottom part of
 +                 * visible area.
 +                 */
 +                if (!isFocusAtTheEndOfTable()) {
 +                    VScrollTableRow lastVisibleRowInViewPort = scrollBody
 +                            .getRowByRowIndex(firstRowInViewPort
 +                                    + getFullyVisibleRowCount() - 1);
 +                    if (lastVisibleRowInViewPort != null
 +                            && lastVisibleRowInViewPort != focusedRow) {
 +                        // focused row is not at the end of the table, move
 +                        // focus and select the last visible row
 +                        setRowFocus(lastVisibleRowInViewPort);
 +                        selectFocusedRow(ctrl, shift);
 +                        sendSelectedRows();
 +                    } else {
 +                        int indexOfToBeFocused = focusedRow.getIndex()
 +                                + getFullyVisibleRowCount();
 +                        if (indexOfToBeFocused >= totalRows) {
 +                            indexOfToBeFocused = totalRows - 1;
 +                        }
 +                        VScrollTableRow toBeFocusedRow = scrollBody
 +                                .getRowByRowIndex(indexOfToBeFocused);
 +
 +                        if (toBeFocusedRow != null) {
 +                            /*
 +                             * if the next focused row is rendered
 +                             */
 +                            setRowFocus(toBeFocusedRow);
 +                            selectFocusedRow(ctrl, shift);
 +                            // TODO needs scrollintoview ?
 +                            sendSelectedRows();
 +                        } else {
 +                            // scroll down by pixels and return, to wait for
 +                            // new rows, then select the last item in the
 +                            // viewport
 +                            selectLastItemInNextRender = true;
 +                            multiselectPending = shift;
 +                            scrollByPagelenght(1);
 +                        }
 +                    }
 +                }
 +            } else {
 +                /* No selections, go page down by scrolling */
 +                scrollByPagelenght(1);
 +            }
 +            return true;
 +        }
 +
 +        // Page Up navigation
 +        if (keycode == getNavigationPageUpKey()) {
 +            if (isSelectable()) {
 +                /*
 +                 * If selectable we plagiate MSW behaviour: first scroll to the
 +                 * end of current view. If at the end, scroll down one page
 +                 * length and keep the selected row in the bottom part of
 +                 * visible area.
 +                 */
 +                if (!isFocusAtTheBeginningOfTable()) {
 +                    VScrollTableRow firstVisibleRowInViewPort = scrollBody
 +                            .getRowByRowIndex(firstRowInViewPort);
 +                    if (firstVisibleRowInViewPort != null
 +                            && firstVisibleRowInViewPort != focusedRow) {
 +                        // focus is not at the beginning of the table, move
 +                        // focus and select the first visible row
 +                        setRowFocus(firstVisibleRowInViewPort);
 +                        selectFocusedRow(ctrl, shift);
 +                        sendSelectedRows();
 +                    } else {
 +                        int indexOfToBeFocused = focusedRow.getIndex()
 +                                - getFullyVisibleRowCount();
 +                        if (indexOfToBeFocused < 0) {
 +                            indexOfToBeFocused = 0;
 +                        }
 +                        VScrollTableRow toBeFocusedRow = scrollBody
 +                                .getRowByRowIndex(indexOfToBeFocused);
 +
 +                        if (toBeFocusedRow != null) { // if the next focused row
 +                                                      // is rendered
 +                            setRowFocus(toBeFocusedRow);
 +                            selectFocusedRow(ctrl, shift);
 +                            // TODO needs scrollintoview ?
 +                            sendSelectedRows();
 +                        } else {
 +                            // unless waiting for the next rowset already
 +                            // scroll down by pixels and return, to wait for
 +                            // new rows, then select the last item in the
 +                            // viewport
 +                            selectFirstItemInNextRender = true;
 +                            multiselectPending = shift;
 +                            scrollByPagelenght(-1);
 +                        }
 +                    }
 +                }
 +            } else {
 +                /* No selections, go page up by scrolling */
 +                scrollByPagelenght(-1);
 +            }
 +
 +            return true;
 +        }
 +
 +        // Goto start navigation
 +        if (keycode == getNavigationStartKey()) {
 +            scrollBodyPanel.setScrollPosition(0);
 +            if (isSelectable()) {
 +                if (focusedRow != null && focusedRow.getIndex() == 0) {
 +                    return false;
 +                } else {
 +                    VScrollTableRow rowByRowIndex = (VScrollTableRow) scrollBody
 +                            .iterator().next();
 +                    if (rowByRowIndex.getIndex() == 0) {
 +                        setRowFocus(rowByRowIndex);
 +                        selectFocusedRow(ctrl, shift);
 +                        sendSelectedRows();
 +                    } else {
 +                        // first row of table will come in next row fetch
 +                        if (ctrl) {
 +                            focusFirstItemInNextRender = true;
 +                        } else {
 +                            selectFirstItemInNextRender = true;
 +                            multiselectPending = shift;
 +                        }
 +                    }
 +                }
 +            }
 +            return true;
 +        }
 +
 +        // Goto end navigation
 +        if (keycode == getNavigationEndKey()) {
 +            scrollBodyPanel.setScrollPosition(scrollBody.getOffsetHeight());
 +            if (isSelectable()) {
 +                final int lastRendered = scrollBody.getLastRendered();
 +                if (lastRendered + 1 == totalRows) {
 +                    VScrollTableRow rowByRowIndex = scrollBody
 +                            .getRowByRowIndex(lastRendered);
 +                    if (focusedRow != rowByRowIndex) {
 +                        setRowFocus(rowByRowIndex);
 +                        selectFocusedRow(ctrl, shift);
 +                        sendSelectedRows();
 +                    }
 +                } else {
 +                    if (ctrl) {
 +                        focusLastItemInNextRender = true;
 +                    } else {
 +                        selectLastItemInNextRender = true;
 +                        multiselectPending = shift;
 +                    }
 +                }
 +            }
 +            return true;
 +        }
 +
 +        return false;
 +    }
 +
 +    private boolean isFocusAtTheBeginningOfTable() {
 +        return focusedRow.getIndex() == 0;
 +    }
 +
 +    private boolean isFocusAtTheEndOfTable() {
 +        return focusedRow.getIndex() + 1 >= totalRows;
 +    }
 +
 +    private int getFullyVisibleRowCount() {
 +        return (int) (scrollBodyPanel.getOffsetHeight() / scrollBody
 +                .getRowHeight());
 +    }
 +
 +    private void scrollByPagelenght(int i) {
 +        int pixels = i * scrollBodyPanel.getOffsetHeight();
 +        int newPixels = scrollBodyPanel.getScrollPosition() + pixels;
 +        if (newPixels < 0) {
 +            newPixels = 0;
 +        } // else if too high, NOP (all know browsers accept illegally big
 +          // values here)
 +        scrollBodyPanel.setScrollPosition(newPixels);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
 +     * .dom.client.FocusEvent)
 +     */
 +    public void onFocus(FocusEvent event) {
 +        if (isFocusable()) {
 +            hasFocus = true;
 +
 +            // Focus a row if no row is in focus
 +            if (focusedRow == null) {
 +                focusRowFromBody();
 +            } else {
 +                setRowFocus(focusedRow);
 +            }
 +        }
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
 +     * .dom.client.BlurEvent)
 +     */
 +    public void onBlur(BlurEvent event) {
 +        hasFocus = false;
 +        navKeyDown = false;
 +
 +        if (BrowserInfo.get().isIE()) {
 +            // IE sometimes moves focus to a clicked table cell...
 +            Element focusedElement = Util.getIEFocusedElement();
 +            if (Util.getConnectorForElement(client, getParent(), focusedElement) == this) {
 +                // ..in that case, steal the focus back to the focus handler
 +                // but not if focus is in a child component instead (#7965)
 +                focus();
 +                return;
 +            }
 +        }
 +
 +        if (isFocusable()) {
 +            // Unfocus any row
 +            setRowFocus(null);
 +        }
 +    }
 +
 +    /**
 +     * Removes a key from a range if the key is found in a selected range
 +     * 
 +     * @param key
 +     *            The key to remove
 +     */
 +    private void removeRowFromUnsentSelectionRanges(VScrollTableRow row) {
 +        Collection<SelectionRange> newRanges = null;
 +        for (Iterator<SelectionRange> iterator = selectedRowRanges.iterator(); iterator
 +                .hasNext();) {
 +            SelectionRange range = iterator.next();
 +            if (range.inRange(row)) {
 +                // Split the range if given row is in range
 +                Collection<SelectionRange> splitranges = range.split(row);
 +                if (newRanges == null) {
 +                    newRanges = new ArrayList<SelectionRange>();
 +                }
 +                newRanges.addAll(splitranges);
 +                iterator.remove();
 +            }
 +        }
 +        if (newRanges != null) {
 +            selectedRowRanges.addAll(newRanges);
 +        }
 +    }
 +
 +    /**
 +     * Can the Table be focused?
 +     * 
 +     * @return True if the table can be focused, else false
 +     */
 +    public boolean isFocusable() {
 +        if (scrollBody != null && enabled) {
 +            return !(!hasHorizontalScrollbar() && !hasVerticalScrollbar() && !isSelectable());
 +        }
 +        return false;
 +    }
 +
 +    private boolean hasHorizontalScrollbar() {
 +        return scrollBody.getOffsetWidth() > scrollBodyPanel.getOffsetWidth();
 +    }
 +
 +    private boolean hasVerticalScrollbar() {
 +        return scrollBody.getOffsetHeight() > scrollBodyPanel.getOffsetHeight();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.terminal.gwt.client.Focusable#focus()
 +     */
 +    public void focus() {
 +        if (isFocusable()) {
 +            scrollBodyPanel.focus();
 +        }
 +    }
 +
 +    /**
 +     * Sets the proper tabIndex for scrollBodyPanel (the focusable elemen in the
 +     * component).
 +     * 
 +     * If the component has no explicit tabIndex a zero is given (default
 +     * tabbing order based on dom hierarchy) or -1 if the component does not
 +     * need to gain focus. The component needs no focus if it has no scrollabars
 +     * (not scrollable) and not selectable. Note that in the future shortcut
 +     * actions may need focus.
 +     * 
 +     */
 +    void setProperTabIndex() {
 +        int storedScrollTop = 0;
 +        int storedScrollLeft = 0;
 +
 +        if (BrowserInfo.get().getOperaVersion() >= 11) {
 +            // Workaround for Opera scroll bug when changing tabIndex (#6222)
 +            storedScrollTop = scrollBodyPanel.getScrollPosition();
 +            storedScrollLeft = scrollBodyPanel.getHorizontalScrollPosition();
 +        }
 +
 +        if (tabIndex == 0 && !isFocusable()) {
 +            scrollBodyPanel.setTabIndex(-1);
 +        } else {
 +            scrollBodyPanel.setTabIndex(tabIndex);
 +        }
 +
 +        if (BrowserInfo.get().getOperaVersion() >= 11) {
 +            // Workaround for Opera scroll bug when changing tabIndex (#6222)
 +            scrollBodyPanel.setScrollPosition(storedScrollTop);
 +            scrollBodyPanel.setHorizontalScrollPosition(storedScrollLeft);
 +        }
 +    }
 +
 +    public void startScrollingVelocityTimer() {
 +        if (scrollingVelocityTimer == null) {
 +            scrollingVelocityTimer = new Timer() {
 +                @Override
 +                public void run() {
 +                    scrollingVelocity++;
 +                }
 +            };
 +            scrollingVelocityTimer.scheduleRepeating(100);
 +        }
 +    }
 +
 +    public void cancelScrollingVelocityTimer() {
 +        if (scrollingVelocityTimer != null) {
 +            // Remove velocityTimer if it exists and the Table is disabled
 +            scrollingVelocityTimer.cancel();
 +            scrollingVelocityTimer = null;
 +            scrollingVelocity = 10;
 +        }
 +    }
 +
 +    /**
 +     * 
 +     * @param keyCode
 +     * @return true if the given keyCode is used by the table for navigation
 +     */
 +    private boolean isNavigationKey(int keyCode) {
 +        return keyCode == getNavigationUpKey()
 +                || keyCode == getNavigationLeftKey()
 +                || keyCode == getNavigationRightKey()
 +                || keyCode == getNavigationDownKey()
 +                || keyCode == getNavigationPageUpKey()
 +                || keyCode == getNavigationPageDownKey()
 +                || keyCode == getNavigationEndKey()
 +                || keyCode == getNavigationStartKey();
 +    }
 +
 +    public void lazyRevertFocusToRow(final VScrollTableRow currentlyFocusedRow) {
 +        Scheduler.get().scheduleFinally(new ScheduledCommand() {
 +            public void execute() {
 +                if (currentlyFocusedRow != null) {
 +                    setRowFocus(currentlyFocusedRow);
 +                } else {
 +                    VConsole.log("no row?");
 +                    focusRowFromBody();
 +                }
 +                scrollBody.ensureFocus();
 +            }
 +        });
 +    }
 +
 +    public Action[] getActions() {
 +        if (bodyActionKeys == null) {
 +            return new Action[] {};
 +        }
 +        final Action[] actions = new Action[bodyActionKeys.length];
 +        for (int i = 0; i < actions.length; i++) {
 +            final String actionKey = bodyActionKeys[i];
 +            Action bodyAction = new TreeAction(this, null, actionKey);
 +            bodyAction.setCaption(getActionCaption(actionKey));
 +            bodyAction.setIconUrl(getActionIcon(actionKey));
 +            actions[i] = bodyAction;
 +        }
 +        return actions;
 +    }
 +
 +    public ApplicationConnection getClient() {
 +        return client;
 +    }
 +
 +    public String getPaintableId() {
 +        return paintableId;
 +    }
 +
 +    /**
 +     * Add this to the element mouse down event by using element.setPropertyJSO
 +     * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again
 +     * when the mouse is depressed in the mouse up event.
 +     * 
 +     * @return Returns the JSO preventing text selection
 +     */
 +    private static native JavaScriptObject getPreventTextSelectionIEHack()
 +    /*-{
 +            return function(){ return false; };
 +    }-*/;
 +
 +    public void triggerLazyColumnAdjustment(boolean now) {
 +        lazyAdjustColumnWidths.cancel();
 +        if (now) {
 +            lazyAdjustColumnWidths.run();
 +        } else {
 +            lazyAdjustColumnWidths.schedule(LAZY_COLUMN_ADJUST_TIMEOUT);
 +        }
 +    }
 +
 +    private boolean isDynamicWidth() {
 +        ComponentConnector paintable = ConnectorMap.get(client).getConnector(
 +                this);
 +        return paintable.isUndefinedWidth();
 +    }
 +
 +    private boolean isDynamicHeight() {
 +        ComponentConnector paintable = ConnectorMap.get(client).getConnector(
 +                this);
 +        if (paintable == null) {
 +            // This should be refactored. As isDynamicHeight can be called from
 +            // a timer it is possible that the connector has been unregistered
 +            // when this method is called, causing getConnector to return null.
 +            return false;
 +        }
 +        return paintable.isUndefinedHeight();
 +    }
 +
 +    private void debug(String msg) {
 +        if (enableDebug) {
 +            VConsole.error(msg);
 +        }
 +    }
 +
 +    public Widget getWidgetForPaintable() {
 +        return this;
 +    }
 +}
index b2141e06e5ccc3b19d5a2ed444ec0755a883bf46,0000000000000000000000000000000000000000..7bd392b5034c779bba95047136058b9e6ad0b9ee
mode 100644,000000..100644
--- /dev/null
@@@ -1,442 -1,0 +1,442 @@@
-         el.oncut = function() {
 +/*
 +@VaadinApache2LicenseForJavaFiles@
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.textfield;
 +
 +import com.google.gwt.dom.client.Style.Overflow;
 +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.VTooltip;
 +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;
 +
 +    protected boolean immediate = false;
 +    private int maxLength = -1;
 +
 +    private static final String CLASSNAME_PROMPT = "prompt";
 +    protected static final String ATTR_INPUTPROMPT = "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";
 +
 +    protected String inputPrompt = null;
 +    private boolean prompting = false;
 +    private int lastCursorPos = -1;
 +    private boolean wordwrap = true;
 +
 +    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);
 +        sinkEvents(VTooltip.TOOLTIP_EVENTS);
 +    }
 +
 +    /*
 +     * 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 (client != null) {
 +            client.handleTooltipEvent(event, this);
 +        }
 +
 +        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;
 +            if (getElement().getTagName().toLowerCase().equals("textarea")) {
 +                // NOP no maxlength property for textarea
 +            } else {
 +                getElement().setPropertyInt("maxLength", maxLength);
 +            }
 +        } else if (maxLength != -1) {
 +            if (getElement().getTagName().toLowerCase().equals("textarea")) {
 +                // NOP no maxlength property for textarea
 +            } else {
 +                getElement().removeAttribute("maxLength");
 +            }
 +            maxLength = -1;
 +        }
 +
 +    }
 +
 +    public int getMaxLength() {
 +        return maxLength;
 +    }
 +
 +    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", getText(), 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);
 +        }
 +    }
 +
 +    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);
 +        }
 +    }
 +
 +    public void onBlur(BlurEvent event) {
 +        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) {
 +        setColumns(getElement(), columns);
 +    }
 +
 +    private native void setColumns(Element e, int c)
 +    /*-{
 +    try {
 +      switch(e.tagName.toLowerCase()) {
 +              case "input":
 +                      //e.size = c;
 +                      e.style.width = c+"em";
 +                      break;
 +              case "textarea":
 +                      //e.cols = c;
 +                      e.style.width = c+"em";
 +                      break;
 +              default:;
 +      }
 +    } catch (e) {}
 +    }-*/;
 +
 +    // Here for backward compatibility; to be moved to TextArea
 +    public void setWordwrap(boolean enabled) {
 +        if (enabled == wordwrap) {
 +            return; // No change
 +        }
 +
 +        if (enabled) {
 +            getElement().removeAttribute("wrap");
 +            getElement().getStyle().clearOverflow();
 +        } else {
 +            getElement().setAttribute("wrap", "off");
 +            getElement().getStyle().setOverflow(Overflow.AUTO);
 +        }
 +        if (BrowserInfo.get().isSafari4()) {
 +            // Force redraw as Safari 4 does not properly update the screen
 +            Util.forceWebkitRedraw(getElement());
 +        } else if (BrowserInfo.get().isOpera()) {
 +            // Opera fails to dynamically update the wrap attribute so we detach
 +            // and reattach the whole TextArea.
 +            Util.detachAttach(getElement());
 +        }
 +        wordwrap = enabled;
 +    }
 +
 +    public void onKeyDown(KeyDownEvent event) {
 +        if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
 +            valueChange(false);
 +        }
 +    }
 +}
index d40f954cdc7822cc438cfcf6c6029addf7cc3dc0,0000000000000000000000000000000000000000..484000b8d11088213f856837b4c2ea1791638003
mode 100644,000000..100644
--- /dev/null
@@@ -1,64 -1,0 +1,64 @@@
-               el.addEventListener('loadedmetadata', function(e) {
-                   $entry(self.@com.vaadin.terminal.gwt.client.ui.video.VVideo::updateElementDynamicSize(II)(el.videoWidth, el.videoHeight));
-               }, false);
 +/*
 +@VaadinApache2LicenseForJavaFiles@
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.video;
 +
 +import com.google.gwt.dom.client.Document;
 +import com.google.gwt.dom.client.Style.Unit;
 +import com.google.gwt.dom.client.VideoElement;
 +import com.google.gwt.user.client.Element;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.ui.VMediaBase;
 +
 +public class VVideo extends VMediaBase {
 +
 +    private static String CLASSNAME = "v-video";
 +
 +    private VideoElement video;
 +
 +    public VVideo() {
 +        video = Document.get().createVideoElement();
 +        setMediaElement(video);
 +        setStyleName(CLASSNAME);
 +
 +        updateDimensionsWhenMetadataLoaded(getElement());
 +    }
 +
 +    /**
 +     * Registers a listener that updates the dimensions of the widget when the
 +     * video metadata has been loaded.
 +     * 
 +     * @param el
 +     */
 +    private native void updateDimensionsWhenMetadataLoaded(Element el)
 +    /*-{
 +              var self = this;
++              el.addEventListener('loadedmetadata', $entry(function(e) {
++                  self.@com.vaadin.terminal.gwt.client.ui.video.VVideo::updateElementDynamicSize(II)(el.videoWidth, el.videoHeight);
++              }), false);
 +
 +    }-*/;
 +
 +    /**
 +     * Updates the dimensions of the widget.
 +     * 
 +     * @param w
 +     * @param h
 +     */
 +    private void updateElementDynamicSize(int w, int h) {
 +        video.getStyle().setWidth(w, Unit.PX);
 +        video.getStyle().setHeight(h, Unit.PX);
 +        Util.notifyParentOfSizeChange(this, true);
 +    }
 +
 +    @Override
 +    protected String getDefaultAltHtml() {
 +        return "Your browser does not support the <code>video</code> element.";
 +    }
 +
 +    public void setPoster(String poster) {
 +        video.setPoster(poster);
 +    }
 +
 +}
index 0d670863399f83bf960819b43b2dce57c2d563ae,a5923cb47f429b2a51135467d6a3e38ccfa40425..58d6a1859288593c37ffd91dad9570aa70350112
@@@ -509,27 -326,11 +509,30 @@@ public abstract class AbstractApplicati
  
      protected void handleRequest(PortletRequest request,
              PortletResponse response) throws PortletException, IOException {
 -        RequestTimer.RequestWrapper wrappedRequest = new RequestTimer.RequestWrapper(
 -                request);
 +        AbstractApplicationPortletWrapper portletWrapper = new AbstractApplicationPortletWrapper(
 +                this);
 +
 +        WrappedPortletRequest wrappedRequest;
 +
 +        String portalInfo = request.getPortalContext().getPortalInfo()
 +                .toLowerCase();
 +        if (portalInfo.contains("liferay")) {
 +            wrappedRequest = new WrappedLiferayRequest(request,
 +                    getDeploymentConfiguration());
 +        } else if (portalInfo.contains("gatein")) {
 +            wrappedRequest = new WrappedGateinRequest(request,
 +                    getDeploymentConfiguration());
 +        } else {
 +            wrappedRequest = new WrappedPortletRequest(request,
 +                    getDeploymentConfiguration());
 +        }
 +
 +        WrappedPortletResponse wrappedResponse = new WrappedPortletResponse(
 +                response, getDeploymentConfiguration());
 +
+         RequestTimer requestTimer = RequestTimer.get(wrappedRequest);
+         requestTimer.start(wrappedRequest);
          RequestType requestType = getRequestType(request);
  
          if (requestType == RequestType.UNKNOWN) {
                                  .endTransaction(application, request);
                      }
                  } finally {
 -                    if (requestStarted) {
 -                        ((PortletRequestListener) application).onRequestEnd(
 -                                request, response);
 +                    try {
 +                        if (requestStarted) {
 +                            ((PortletRequestListener) application)
 +                                    .onRequestEnd(request, response);
  
 +                        }
 +                    } finally {
 +                        Root.setCurrentRoot(null);
 +                        Application.setCurrentApplication(null);
                      }
+                     requestTimer.stop();
+                     RequestTimer.set(wrappedRequest, requestTimer);
                  }
              }
          }
index 18cc3f97f405575ab2257fa20d50b76fb9a6b58f,04ea423004783f4ad4a3565db91c05e3ab877a1d..799271b9795996cc472c36e1c9e437df488f0278
@@@ -394,15 -400,12 +394,18 @@@ public abstract class AbstractApplicati
      @Override
      protected void service(HttpServletRequest request,
              HttpServletResponse response) throws ServletException, IOException {
 -        RequestTimer.RequestWrapper wrappedRequest = new RequestTimer.RequestWrapper(
 -                request);
 -        RequestTimer requestTimer = RequestTimer.get(wrappedRequest);
 -        requestTimer.start(wrappedRequest);
 +        service(createWrappedRequest(request), createWrappedResponse(response));
 +    }
 +
 +    private void service(WrappedHttpServletRequest request,
 +            WrappedHttpServletResponse response) throws ServletException,
 +            IOException {
 +        AbstractApplicationServletWrapper servletWrapper = new AbstractApplicationServletWrapper(
 +                this);
 +
++        RequestTimer requestTimer = RequestTimer.get(request);
++        requestTimer.start(request);
          RequestType requestType = getRequestType(request);
          if (!ensureCookiesEnabled(requestType, request, response)) {
              return;
                  }
  
              } finally {
 -                if (requestStarted) {
 -                    ((HttpServletRequestListener) application).onRequestEnd(
 -                            request, response);
 +                try {
 +                    if (requestStarted) {
 +                        ((HttpServletRequestListener) application)
 +                                .onRequestEnd(request, response);
 +                    }
 +                } finally {
 +                    Root.setCurrentRoot(null);
 +                    Application.setCurrentApplication(null);
                  }
  
 -                RequestTimer.set(wrappedRequest, requestTimer);
+                 requestTimer.stop();
++                RequestTimer.set(request, requestTimer);
              }
  
          }
index a1a917130f02407225c9fc07af84c0dba1a1e451,9a6bccebb81f14353b1ebad1f692d0a6b8787310..b780f66a23c3942621766143fc32579caf5fe7bc
@@@ -688,65 -886,64 +688,65 @@@ public abstract class AbstractCommunica
                  .getAttribute(WRITE_SECURITY_TOKEN_FLAG);
  
          if (writeSecurityTokenFlag != null) {
 -            String seckey = (String) request.getSession().getAttribute(
 -                    ApplicationConnection.UIDL_SECURITY_TOKEN_ID);
 -            if (seckey == null) {
 -                seckey = UUID.randomUUID().toString();
 -                request.getSession().setAttribute(
 -                        ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey);
 -            }
 -            outWriter.print("\"" + ApplicationConnection.UIDL_SECURITY_TOKEN_ID
 -                    + "\":\"");
 -            outWriter.print(seckey);
 -            outWriter.print("\",");
 +            outWriter.print(getSecurityKeyUIDL(request));
          }
  
-         writeUidlResponse(repaintAll, outWriter, root, analyzeLayouts);
 -        // If the browser-window has been closed - we do not need to paint it at
 -        // all
 -        if (window.getName().equals(closingWindowName)) {
 -            outWriter.print("\"changes\":[]");
 -        } else {
 -            // re-get window - may have been changed
 -            Window newWindow = doGetApplicationWindow(request, callback,
 -                    application, window);
 -            if (newWindow != window) {
 -                window = newWindow;
 -                repaintAll = true;
 -            }
 -
 -            writeUidlResponse(request, callback, repaintAll, outWriter, window,
 -                    analyzeLayouts);
++        writeUidlResponse(request, repaintAll, outWriter, root, analyzeLayouts);
  
 -        }
          closeJsonMessage(outWriter);
  
          outWriter.close();
  
      }
  
 -    public void writeUidlResponse(Request request, Callback callback,
 -            boolean repaintAll, final PrintWriter outWriter, Window window,
 -            boolean analyzeLayouts) throws PaintException {
 -        outWriter.print("\"changes\":[");
 -
 -        ArrayList<Paintable> paintables = null;
 -
 -        List<InvalidLayout> invalidComponentRelativeSizes = null;
 +    /**
 +     * Gets the security key (and generates one if needed) as UIDL.
 +     * 
 +     * @param request
 +     * @return the security key UIDL or "" if the feature is turned off
 +     */
 +    public String getSecurityKeyUIDL(WrappedRequest request) {
 +        final String seckey = getSecurityKey(request);
 +        if (seckey != null) {
 +            return "\"" + ApplicationConnection.UIDL_SECURITY_TOKEN_ID
 +                    + "\":\"" + seckey + "\",";
 +        } else {
 +            return "";
 +        }
 +    }
  
 -        JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter,
 -                !repaintAll);
 -        OpenWindowCache windowCache = currentlyOpenWindowsInClient.get(window
 -                .getName());
 -        if (windowCache == null) {
 -            windowCache = new OpenWindowCache();
 -            currentlyOpenWindowsInClient.put(window.getName(), windowCache);
 +    /**
 +     * Gets the security key (and generates one if needed).
 +     * 
 +     * @param request
 +     * @return the security key
 +     */
 +    protected String getSecurityKey(WrappedRequest request) {
 +        String seckey = null;
 +        seckey = (String) request
 +                .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID);
 +        if (seckey == null) {
 +            seckey = UUID.randomUUID().toString();
 +            request.setSessionAttribute(
 +                    ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey);
          }
  
-     public void writeUidlResponse(boolean repaintAll,
 +        return seckey;
 +    }
 +
 +    @SuppressWarnings("unchecked")
++    public void writeUidlResponse(WrappedRequest request, boolean repaintAll,
 +            final PrintWriter outWriter, Root root, boolean analyzeLayouts)
 +            throws PaintException {
 +        ArrayList<ClientConnector> dirtyVisibleConnectors = new ArrayList<ClientConnector>();
 +        Application application = root.getApplication();
          // Paints components
 +        DirtyConnectorTracker rootConnectorTracker = root
 +                .getDirtyConnectorTracker();
 +        logger.log(Level.FINE, "* Creating response to client");
          if (repaintAll) {
 -            paintables = new ArrayList<Paintable>();
 -            paintables.add(window);
 +            getClientCache(root).clear();
 +            rootConnectorTracker.markAllComponentsDirty();
  
              // Reset sent locales
              locales = null;
          if (dragAndDropService != null) {
              dragAndDropService.printJSONResponse(outWriter);
          }
 -    private void writePerformanceDataForTestBench(final Request request,
+         writePerformanceDataForTestBench(request, outWriter);
+     }
+     /**
+      * Adds the performance timing data used by TestBench 3 to the UIDL
+      * response.
+      */
++    private void writePerformanceDataForTestBench(final WrappedRequest request,
+             final PrintWriter outWriter) {
+         Long totalTime = (Long) request.getAttribute("TOTAL");
+         Long lastRequestTime = (Long) request.getAttribute("LASTREQUEST");
+         outWriter.write(String.format(", \"tbss\":[%d, %d]", totalTime,
+                 lastRequestTime));
      }
  
 +    private void legacyPaint(PaintTarget paintTarget,
 +            ArrayList<ClientConnector> dirtyVisibleConnectors)
 +            throws PaintException {
 +        List<Vaadin6Component> legacyComponents = new ArrayList<Vaadin6Component>();
 +        for (Connector connector : dirtyVisibleConnectors) {
 +            // All Components that want to use paintContent must implement
 +            // Vaadin6Component
 +            if (connector instanceof Vaadin6Component) {
 +                legacyComponents.add((Vaadin6Component) connector);
 +            }
 +        }
 +        sortByHierarchy((List) legacyComponents);
 +        for (Vaadin6Component c : legacyComponents) {
 +            logger.fine("Painting Vaadin6Component " + c.getClass().getName()
 +                    + "@" + Integer.toHexString(c.hashCode()));
 +            paintTarget.startTag("change");
 +            final String pid = c.getConnectorId();
 +            paintTarget.addAttribute("pid", pid);
 +            LegacyPaint.paint(c, paintTarget);
 +            paintTarget.endTag("change");
 +        }
 +
 +    }
 +
 +    private void sortByHierarchy(List<Component> paintables) {
 +        // Vaadin 6 requires parents to be painted before children as component
 +        // containers rely on that their updateFromUIDL method has been called
 +        // before children start calling e.g. updateCaption
 +        Collections.sort(paintables, new Comparator<Component>() {
 +            public int compare(Component c1, Component c2) {
 +                int depth1 = 0;
 +                while (c1.getParent() != null) {
 +                    depth1++;
 +                    c1 = c1.getParent();
 +                }
 +                int depth2 = 0;
 +                while (c2.getParent() != null) {
 +                    depth2++;
 +                    c2 = c2.getParent();
 +                }
 +                if (depth1 < depth2) {
 +                    return -1;
 +                }
 +                if (depth1 > depth2) {
 +                    return 1;
 +                }
 +                return 0;
 +            }
 +        });
 +
 +    }
 +
 +    private ClientCache getClientCache(Root root) {
 +        Integer rootId = Integer.valueOf(root.getRootId());
 +        ClientCache cache = rootToClientCache.get(rootId);
 +        if (cache == null) {
 +            cache = new ClientCache();
 +            rootToClientCache.put(rootId, cache);
 +        }
 +        return cache;
 +    }
 +
 +    /**
 +     * Checks if the component is visible in context, i.e. returns false if the
 +     * child is hidden, the parent is hidden or the parent says the child should
 +     * not be rendered (using
 +     * {@link HasComponents#isComponentVisible(Component)}
 +     * 
 +     * @param child
 +     *            The child to check
 +     * @return true if the child is visible to the client, false otherwise
 +     */
 +    static boolean isVisible(Component child) {
 +        HasComponents parent = child.getParent();
 +        if (parent == null || !child.isVisible()) {
 +            return child.isVisible();
 +        }
 +
 +        return parent.isComponentVisible(child) && isVisible(parent);
 +    }
 +
 +    private static class NullIterator<E> implements Iterator<E> {
 +
 +        public boolean hasNext() {
 +            return false;
 +        }
 +
 +        public E next() {
 +            return null;
 +        }
 +
 +        public void remove() {
 +        }
 +
 +    }
 +
 +    public static Iterable<Component> getChildComponents(HasComponents cc) {
 +        // TODO This must be moved to Root/Panel
 +        if (cc instanceof Root) {
 +            Root root = (Root) cc;
 +            List<Component> children = new ArrayList<Component>();
 +            if (root.getContent() != null) {
 +                children.add(root.getContent());
 +            }
 +            for (Window w : root.getWindows()) {
 +                children.add(w);
 +            }
 +            return children;
 +        } else if (cc instanceof Panel) {
 +            // This is so wrong.. (#2924)
 +            if (((Panel) cc).getContent() == null) {
 +                return Collections.emptyList();
 +            } else {
 +                return Collections.singleton((Component) ((Panel) cc)
 +                        .getContent());
 +            }
 +        }
 +        return cc;
 +    }
 +
 +    /**
 +     * Collects all pending RPC calls from listed {@link ClientConnector}s and
 +     * clears their RPC queues.
 +     * 
 +     * @param rpcPendingQueue
 +     *            list of {@link ClientConnector} of interest
 +     * @return ordered list of pending RPC calls
 +     */
 +    private List<ClientMethodInvocation> collectPendingRpcCalls(
 +            List<ClientConnector> rpcPendingQueue) {
 +        List<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>();
 +        for (ClientConnector connector : rpcPendingQueue) {
 +            List<ClientMethodInvocation> paintablePendingRpc = connector
 +                    .retrievePendingRpcCalls();
 +            if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) {
 +                List<ClientMethodInvocation> oldPendingRpc = pendingInvocations;
 +                int totalCalls = pendingInvocations.size()
 +                        + paintablePendingRpc.size();
 +                pendingInvocations = new ArrayList<ClientMethodInvocation>(
 +                        totalCalls);
 +
 +                // merge two ordered comparable lists
 +                for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) {
 +                    if (paintableIndex >= paintablePendingRpc.size()
 +                            || (oldIndex < oldPendingRpc.size() && ((Comparable<ClientMethodInvocation>) oldPendingRpc
 +                                    .get(oldIndex))
 +                                    .compareTo(paintablePendingRpc
 +                                            .get(paintableIndex)) <= 0)) {
 +                        pendingInvocations.add(oldPendingRpc.get(oldIndex++));
 +                    } else {
 +                        pendingInvocations.add(paintablePendingRpc
 +                                .get(paintableIndex++));
 +                    }
 +                }
 +            }
 +        }
 +        return pendingInvocations;
 +    }
 +
 +    protected abstract InputStream getThemeResourceAsStream(Root root,
 +            String themeName, String resource);
 +
      private int getTimeoutInterval() {
          return maxInactiveInterval;
      }
  
      }
  
 -    abstract String getStreamVariableTargetUrl(VariableOwner owner,
 -            String name, StreamVariable value);
 +    abstract String getStreamVariableTargetUrl(Connector owner, String name,
 +            StreamVariable value);
  
 -    abstract protected void cleanStreamVariable(VariableOwner owner, String name);
 +    abstract protected void cleanStreamVariable(Connector owner, String name);
 +
 +    /**
 +     * Gets the bootstrap handler that should be used for generating the pages
 +     * bootstrapping applications for this communication manager.
 +     * 
 +     * @return the bootstrap handler to use
 +     */
 +    private BootstrapHandler getBootstrapHandler() {
 +        if (bootstrapHandler == null) {
 +            bootstrapHandler = createBootstrapHandler();
 +        }
 +
 +        return bootstrapHandler;
 +    }
 +
 +    protected abstract BootstrapHandler createBootstrapHandler();
 +
 +    protected boolean handleApplicationRequest(WrappedRequest request,
 +            WrappedResponse response) throws IOException {
 +        return application.handleRequest(request, response);
 +    }
 +
 +    public void handleBrowserDetailsRequest(WrappedRequest request,
 +            WrappedResponse response, Application application)
 +            throws IOException {
 +
 +        // if we do not yet have a currentRoot, it should be initialized
 +        // shortly, and we should send the initial UIDL
 +        boolean sendUIDL = Root.getCurrentRoot() == null;
 +
 +        try {
 +            CombinedRequest combinedRequest = new CombinedRequest(request);
 +
 +            Root root = application.getRootForRequest(combinedRequest);
 +            response.setContentType("application/json; charset=UTF-8");
 +
 +            // Use the same logic as for determined roots
 +            BootstrapHandler bootstrapHandler = getBootstrapHandler();
 +            BootstrapContext context = bootstrapHandler.createContext(
 +                    combinedRequest, response, application, root.getRootId());
 +
 +            String widgetset = context.getWidgetsetName();
 +            String theme = context.getThemeName();
 +            String themeUri = bootstrapHandler.getThemeUri(context, theme);
 +
 +            // TODO These are not required if it was only the init of the root
 +            // that was delayed
 +            JSONObject params = new JSONObject();
 +            params.put("widgetset", widgetset);
 +            params.put("themeUri", themeUri);
 +            // Root id might have changed based on e.g. window.name
 +            params.put(ApplicationConnection.ROOT_ID_PARAMETER,
 +                    root.getRootId());
 +            if (sendUIDL) {
 +                String initialUIDL = getInitialUIDL(combinedRequest, root);
 +                params.put("uidl", initialUIDL);
 +            }
 +
 +            // NOTE! GateIn requires, for some weird reason, getOutputStream
 +            // to be used instead of getWriter() (it seems to interpret
 +            // application/json as a binary content type)
 +            final OutputStream out = response.getOutputStream();
 +            final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
 +                    new OutputStreamWriter(out, "UTF-8")));
 +
 +            outWriter.write(params.toString());
 +            // NOTE GateIn requires the buffers to be flushed to work
 +            outWriter.flush();
 +            out.flush();
 +        } catch (RootRequiresMoreInformationException e) {
 +            // Requiring more information at this point is not allowed
 +            // TODO handle in a better way
 +            throw new RuntimeException(e);
 +        } catch (JSONException e) {
 +            // TODO Auto-generated catch block
 +            e.printStackTrace();
 +        }
 +    }
 +
 +    /**
 +     * Generates the initial UIDL message that can e.g. be included in a html
 +     * page to avoid a separate round trip just for getting the UIDL.
 +     * 
 +     * @param request
 +     *            the request that caused the initialization
 +     * @param root
 +     *            the root for which the UIDL should be generated
 +     * @return a string with the initial UIDL message
 +     * @throws PaintException
 +     *             if an exception occurs while painting
 +     */
 +    protected String getInitialUIDL(WrappedRequest request, Root root)
 +            throws PaintException {
 +        // TODO maybe unify writeUidlResponse()?
 +        StringWriter sWriter = new StringWriter();
 +        PrintWriter pWriter = new PrintWriter(sWriter);
 +        pWriter.print("{");
 +        if (isXSRFEnabled(root.getApplication())) {
 +            pWriter.print(getSecurityKeyUIDL(request));
 +        }
-         writeUidlResponse(true, pWriter, root, false);
++        writeUidlResponse(request, true, pWriter, root, false);
 +        pWriter.print("}");
 +        String initialUIDL = sWriter.toString();
 +        logger.log(Level.FINE, "Initial UIDL:" + initialUIDL);
 +        return initialUIDL;
 +    }
  
      /**
       * Stream that extracts content from another stream until the boundary
index 0000000000000000000000000000000000000000,5ed89c2d29cbdd695f4c0f86fbade24b7225efc9..d47f444bef1b8fafb7dc14b9a54b38edcec631fb
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,142 +1,80 @@@
 -import javax.portlet.PortletRequest;
 -import javax.portlet.PortletSession;
 -import javax.servlet.http.HttpServletRequest;
 -import javax.servlet.http.HttpSession;
+ /*
+ @VaadinApache2LicenseForJavaFiles@
+  */
+ package com.vaadin.terminal.gwt.server;
 -    /**
 -     * This class acts as a proxy for setting and getting session and request
 -     * attributes on HttpServletRequests and PortletRequests. Using this class
 -     * we don't need to duplicate code everywhere.
 -     */
 -    static class RequestWrapper {
 -        private final HttpServletRequest servletRequest;
 -        private final PortletRequest portletRequest;
 -
 -        public RequestWrapper(HttpServletRequest servletRequest) {
 -            this.servletRequest = servletRequest;
 -            portletRequest = null;
 -        }
 -
 -        public RequestWrapper(PortletRequest portletRequest) {
 -            this.portletRequest = portletRequest;
 -            servletRequest = null;
 -        }
 -
 -        public void setAttribute(String name, Object value) {
 -            if (servletRequest != null) {
 -                servletRequest.setAttribute(name, value);
 -            } else {
 -                portletRequest.setAttribute(name, value);
 -            }
 -        }
 -
 -        public void setSessionAttribute(String name, Object value) {
 -            if (servletRequest != null) {
 -                HttpSession session = servletRequest.getSession();
 -                if (session != null) {
 -                    session.setAttribute(name, value);
 -                }
 -            } else {
 -                PortletSession portletSession = portletRequest
 -                        .getPortletSession();
 -                if (portletSession != null) {
 -                    portletSession.setAttribute(name, value);
 -                }
 -            }
 -        }
 -
 -        public Object getSessionAttribute(String name) {
 -            if (servletRequest != null) {
 -                HttpSession session = servletRequest.getSession();
 -                if (session != null) {
 -                    return session.getAttribute(name);
 -                }
 -            } else {
 -                PortletSession portletSession = portletRequest
 -                        .getPortletSession();
 -                if (portletSession != null) {
 -                    return portletSession.getAttribute(name);
 -                }
 -            }
 -            return null;
 -        }
 -    }
 -
++import com.vaadin.terminal.WrappedRequest;
+ /**
+  * Times the handling of requests and stores the information as an attribute in
+  * the request. The timing info is later passed on to the client in the UIDL and
+  * the client provides JavaScript API for accessing this data from e.g.
+  * TestBench.
+  * 
+  * @author Jonatan Kronqvist / Vaadin Ltd
+  */
+ public class RequestTimer {
+     public static final String SESSION_ATTR_ID = "REQUESTTIMER";
+     private long requestStartTime = 0;
+     private long totalSessionTime = 0;
+     private long lastRequestTime = -1;
 -    public void start(RequestWrapper request) {
+     /**
+      * Starts the timing of a request. This should be called before any
+      * processing of the request.
+      * 
+      * @param request
+      *            the request.
+      */
 -    public static RequestTimer get(RequestWrapper request) {
++    public void start(WrappedRequest request) {
+         requestStartTime = System.nanoTime();
+         request.setAttribute("TOTAL", totalSessionTime);
+         request.setAttribute("LASTREQUEST", lastRequestTime);
+     }
+     /**
+      * Stops the timing of a request. This should be called when all processing
+      * of a request has finished.
+      */
+     public void stop() {
+         // Measure and store the total handling time. This data can be
+         // used in TestBench 3 tests.
+         long time = (System.nanoTime() - requestStartTime) / 1000000;
+         lastRequestTime = time;
+         totalSessionTime += time;
+     }
+     /**
+      * Returns a valid request timer for the specified request. Timers are
+      * session bound.
+      * 
+      * @param request
+      *            the request for which to get a valid timer.
+      * @return a valid timer.
+      */
 -    public static void set(RequestWrapper request, RequestTimer requestTimer) {
++    public static RequestTimer get(WrappedRequest request) {
+         RequestTimer timer = (RequestTimer) request
+                 .getSessionAttribute(SESSION_ATTR_ID);
+         if (timer == null) {
+             timer = new RequestTimer();
+         }
+         return timer;
+     }
+     /**
+      * Associates the specified request timer with the specified request. Since
+      * {@link #get(RequestWrapper)} will, at one point or another, return a new
+      * instance, this method should be called to keep the request <-> timer
+      * association updated.
+      * 
+      * @param request
+      *            the request for which to set the timer.
+      * @param requestTimer
+      *            the timer.
+      */
++    public static void set(WrappedRequest request, RequestTimer requestTimer) {
+         request.setSessionAttribute(RequestTimer.SESSION_ATTR_ID, requestTimer);
+     }
+ }
index 0000000000000000000000000000000000000000,88335a19961b9c935e3b12c041d544d87f76a03e..636bddead8b2716597ca79b1fa33c2faeee20668
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,268 +1,267 @@@
 -        p.setScrollable(true);
+ package com.vaadin.tests.components;
+ import java.util.Collection;
+ import com.vaadin.data.Item;
+ import com.vaadin.data.util.IndexedContainer;
+ import com.vaadin.event.Action;
+ import com.vaadin.event.Action.Handler;
+ import com.vaadin.event.DataBoundTransferable;
+ import com.vaadin.event.dd.DragAndDropEvent;
+ import com.vaadin.event.dd.DropHandler;
+ import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
+ import com.vaadin.event.dd.acceptcriteria.SourceIs;
+ import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation;
+ import com.vaadin.tests.util.Person;
+ import com.vaadin.tests.util.PersonContainer;
+ import com.vaadin.tests.util.TestUtils;
+ import com.vaadin.ui.AbstractComponent;
+ import com.vaadin.ui.AbstractSelect.AbstractSelectTargetDetails;
+ import com.vaadin.ui.Button;
+ import com.vaadin.ui.Button.ClickEvent;
+ import com.vaadin.ui.Component;
+ import com.vaadin.ui.CssLayout;
+ import com.vaadin.ui.HorizontalLayout;
+ import com.vaadin.ui.Label;
+ import com.vaadin.ui.Panel;
+ import com.vaadin.ui.Table;
+ public class TouchScrollables extends TestBase {
+     java.util.Random r = new java.util.Random(1);
+     private HorizontalLayout testSelector;
+     @Override
+     public void setup() {
+         testSelector = new HorizontalLayout();
+         getLayout().addComponent(testSelector);
+         CssLayout cssLayout = new CssLayout();
+         final Table table = new Table();
+         Button button = new Button("Toggle lazyloading");
+         button.addListener(new Button.ClickListener() {
+             public void buttonClick(ClickEvent event) {
+                 if (table.getCacheRate() == 100) {
+                     table.setCacheRate(2);
+                     table.setPageLength(15);
+                 } else {
+                     table.setCacheRate(100);
+                     table.setHeight("400px");
+                 }
+             }
+         });
+         cssLayout.addComponent(button);
+         button = new Button("Toggle selectable");
+         button.addListener(new Button.ClickListener() {
+             public void buttonClick(ClickEvent event) {
+                 table.setSelectable(!table.isSelectable());
+             }
+         });
+         cssLayout.addComponent(button);
+         table.addContainerProperty("foo", String.class, "bar");
+         table.setRowHeaderMode(Table.ROW_HEADER_MODE_INDEX);
+         for (int i = 0; i < 1000; i++) {
+             table.addItem();
+         }
+         cssLayout.addComponent(table);
+         cssLayout.setCaption("Table");
+         addTest(cssLayout);
+         cssLayout = new CssLayout();
+         cssLayout.setCaption("Panel");
+         final Panel p = new Panel();
 -                getLayout().getWindow().scrollIntoView(l);
+         p.setHeight("400px");
+         p.setCaption("Panel");
+         Label l50 = null;
+         for (int i = 0; i < 100; i++) {
+             Label c = new Label("Label" + i);
+             p.addComponent(c);
+             if (i == 50) {
+                 l50 = c;
+             }
+         }
+         final Label l = l50;
+         button = new Button("Scroll to label 50", new Button.ClickListener() {
+             public void buttonClick(ClickEvent event) {
 -                        getLayout().getWindow(),
++                getLayout().getRoot().scrollIntoView(l);
+             }
+         });
+         cssLayout.addComponent(button);
+         button = new Button("Scroll to 100px", new Button.ClickListener() {
+             public void buttonClick(ClickEvent event) {
+                 p.setScrollTop(100);
+             }
+         });
+         cssLayout.addComponent(button);
+         cssLayout.addComponent(p);
+         addTest(cssLayout);
+         TestUtils
+                 .injectCSS(
 -                getLayout().getWindow().showNotification(action.getCaption());
++                        getLayout().getRoot(),
+                         "body * {-webkit-user-select: none;} .v-table-row-drag-middle .v-table-cell-content {"
+                                 + "        background-color: inherit ; border-bottom: 1px solid cyan;"
+                                 + "}"
+                                 + ".v-table-row-drag-middle .v-table-cell-wrapper {"
+                                 + "        margin-bottom: -1px;" + "}" + ""
+                 );
+         addDDSortableTable();
+     }
+     private void addDDSortableTable() {
+         final Table table;
+         table = new Table();
+         table.setCaption("DD sortable table with context menus");
+         // table.setWidth("100%");
+         table.setPageLength(10);
+         table.setRowHeaderMode(Table.ROW_HEADER_MODE_ID);
+         table.setSelectable(true);
+         table.setMultiSelect(true);
+         table.addActionHandler(new Handler() {
+             Action[] actions = new Action[] { new Action("FOO"),
+                     new Action("BAR"), new Action("CAR") };
+             public Action[] getActions(Object target, Object sender) {
+                 return actions;
+             }
+             public void handleAction(Action action, Object sender, Object target) {
++                getLayout().getRoot().showNotification(action.getCaption());
+             }
+         });
+         populateTable(table);
+         /*
+          * Make table rows draggable
+          */
+         table.setDragMode(Table.TableDragMode.ROW);
+         table.setDropHandler(new DropHandler() {
+             // accept only drags from this table
+             AcceptCriterion crit = new SourceIs(table);
+             public AcceptCriterion getAcceptCriterion() {
+                 return crit;
+             }
+             public void drop(DragAndDropEvent dropEvent) {
+                 AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dropEvent
+                         .getTargetDetails();
+                 DataBoundTransferable transferable = (DataBoundTransferable) dropEvent
+                         .getTransferable();
+                 Object itemIdOver = dropTargetData.getItemIdOver();
+                 Object itemId = transferable.getItemId();
+                 if (itemId == null || itemIdOver == null
+                         || itemId.equals(itemIdOver)) {
+                     return; // no move happened
+                 }
+                 // IndexedContainer goodies... (hint: don't use it in real apps)
+                 IndexedContainer containerDataSource = (IndexedContainer) table
+                         .getContainerDataSource();
+                 int newIndex = containerDataSource.indexOfId(itemIdOver) - 1;
+                 if (dropTargetData.getDropLocation() != VerticalDropLocation.TOP) {
+                     newIndex++;
+                 }
+                 if (newIndex < 0) {
+                     newIndex = 0;
+                 }
+                 Object idAfter = containerDataSource.getIdByIndex(newIndex);
+                 Collection<?> selections = (Collection<?>) table.getValue();
+                 if (selections != null && selections.contains(itemId)) {
+                     // dragged a selected item, if multiple rows selected, drag
+                     // them too (functionality similar to apple mail)
+                     for (Object object : selections) {
+                         moveAfter(containerDataSource, object, idAfter);
+                     }
+                 } else {
+                     // move just the dragged row, not considering selection at
+                     // all
+                     moveAfter(containerDataSource, itemId, idAfter);
+                 }
+             }
+             private void moveAfter(IndexedContainer containerDataSource,
+                     Object itemId, Object idAfter) {
+                 try {
+                     IndexedContainer clone = null;
+                     clone = (IndexedContainer) containerDataSource.clone();
+                     containerDataSource.removeItem(itemId);
+                     Item newItem = containerDataSource.addItemAfter(idAfter,
+                             itemId);
+                     Item item = clone.getItem(itemId);
+                     for (Object propId : item.getItemPropertyIds()) {
+                         newItem.getItemProperty(propId).setValue(
+                                 item.getItemProperty(propId).getValue());
+                     }
+                     // TODO Auto-generated method stub
+                 } catch (CloneNotSupportedException e) {
+                     // TODO Auto-generated catch block
+                     e.printStackTrace();
+                 }
+             }
+         });
+         addTest(table);
+     }
+     private void populateTable(Table table) {
+         table.addContainerProperty("Name", String.class, "");
+         table.addContainerProperty("Weight", Integer.class, 0);
+         PersonContainer testData = PersonContainer.createWithTestData();
+         for (int i = 0; i < 40; i++) {
+             Item addItem = table.addItem("Item" + i);
+             Person p = testData.getIdByIndex(i);
+             addItem.getItemProperty("Name").setValue(
+                     p.getFirstName() + " " + p.getLastName());
+             addItem.getItemProperty("Weight").setValue(50 + r.nextInt(60));
+         }
+     }
+     private Component testComponent;
+     private void addTest(final AbstractComponent t) {
+         Button button = new Button(t.getCaption());
+         testSelector.addComponent(button);
+         button.addListener(new Button.ClickListener() {
+             public void buttonClick(ClickEvent event) {
+                 if (testComponent != null) {
+                     getLayout().removeComponent(testComponent);
+                 }
+                 testComponent = t;
+                 getLayout().addComponent(t);
+             }
+         });
+     }
+     @Override
+     protected String getDescription() {
+         return "Various components and setups suitable for testing scrolling on touch devices.";
+     }
+     @Override
+     protected Integer getTicketNumber() {
+         return null;
+     }
+ }
index 1748c27426348d71b9d7f0104bf423541e3a5b33,2bc03f98143f1af84f93fed95bd7e760e0b59ffd..7fb096739a3a245fcb286211819a099e09958441
@@@ -7,12 -7,10 +7,12 @@@ import com.vaadin.tests.components.Test
  import com.vaadin.ui.Button;
  import com.vaadin.ui.Button.ClickEvent;
  import com.vaadin.ui.Button.ClickListener;
 +import com.vaadin.ui.Root.LegacyWindow;
+ import com.vaadin.ui.HorizontalLayout;
  import com.vaadin.ui.Table;
 +import com.vaadin.ui.VerticalLayout;
  
- public class TestCurrentPageFirstItem extends Application.LegacyApplication
-         implements ClickListener {
+ public class TestCurrentPageFirstItem extends TestBase implements ClickListener {
  
      private Button buttonIndex;
      private Button buttonItem;