]> source.dussan.org Git - vaadin-framework.git/commitdiff
Merged 6.8 branch
authorJohn Ahlroos <john@vaadin.com>
Mon, 20 Aug 2012 13:07:47 +0000 (16:07 +0300)
committerJohn Ahlroos <john@vaadin.com>
Mon, 20 Aug 2012 13:07:47 +0000 (16:07 +0300)
19 files changed:
1  2 
WebContent/release-notes.html
build/bin/package-diff.py
build/build.xml
client/src/com/vaadin/terminal/gwt/client/BrowserInfo.java
client/src/com/vaadin/terminal/gwt/client/ui/VOverlay.java
client/src/com/vaadin/terminal/gwt/client/ui/menubar/VMenuBar.java
client/src/com/vaadin/terminal/gwt/client/ui/notification/VNotification.java
client/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java
client/src/com/vaadin/terminal/gwt/client/ui/textarea/VTextArea.java
client/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java
client/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java
client/src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java
client/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java
server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java
server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java
server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java
tests/testbench/com/vaadin/tests/components/embedded/EmbeddedApplet.java
tests/testbench/com/vaadin/tests/components/notification/SemiTransparentNotification.java
tests/testbench/com/vaadin/tests/components/popupview/PopupViewClickShortcut.java

index 1e4016db90f91f2fc012ce1cd75eaa018c875e2d,f9aaf12b1b4fcd984b7784dfda0e5ab65c21ea83..c73f9701c5376cdee95dbd5a131d3a5554faf523
        </p>
  
        <ul>
++<<<<<<< HEAD
 +        <li>Vaadin server and client side classes (<tt>/com</tt>)</li>
 +        <li>Vaadin server and client side sources (<tt>/com</tt>)</li>
 +        <li>The default widget set (<tt>/VAADIN/widgetsets</tt>)</li>
 +        <li>Themes: Runo, Reindeer and Chameleon (<tt>/VAADIN/themes</tt>)</li>
 +        <li>Release notes (<tt>/release-notes.html</tt>)</li>
 +        <li>Licensing information (<tt>/license.html</tt>)</li>
 +      </ul>
++=======
+   <li><a href="http://dev.vaadin.com/ticket/8932">#8932</a>: Custom build from 6.8</li>
+   <li><a href="http://dev.vaadin.com/ticket/8193">#8193</a>: Enter ClickShortcut not working in a PopupView</li>
+   <li><a href="http://dev.vaadin.com/ticket/8584">#8584</a>: Invalid behavior of multiselection for com.vaadin.ui.Table</li>
+   <li><a href="http://dev.vaadin.com/ticket/9136">#9136</a>: Tree throws wrong ItemClickEvent in IE9 (when immediate is set to true)</li>
+   <li><a href="http://dev.vaadin.com/ticket/9139">#9139</a>: TextArea in Internet Explorer</li>
+   <li><a href="http://dev.vaadin.com/ticket/9188">#9188</a>: Error parsing maps with empty string as the last value</li>
+   <li><a href="http://dev.vaadin.com/ticket/7036">#7036</a>: Enable scrollbars on a read-only RichTextArea</li>
+   <li><a href="http://dev.vaadin.com/ticket/8109">#8109</a>: Iterating over Table multiselection causes multiple Vaadin SQLContainer full table scans</li>
+   <li><a href="http://dev.vaadin.com/ticket/8144">#8144</a>: Can't use LoginForm inside an iframe</li>
+   <li><a href="http://dev.vaadin.com/ticket/9202">#9202</a>: Test using Chrome 21</li>
+   <li><a href="http://dev.vaadin.com/ticket/6219">#6219</a>: PDF viewer draws itself over floating Vaadin elements</li>
+   <li><a href="http://dev.vaadin.com/ticket/8230">#8230</a>: TextField's input prompt is persisted if Alt is depressed while in focus (Chrome)</li>
+   <li><a href="http://dev.vaadin.com/ticket/8399">#8399</a>: On IE 8 or Google Chrome 16, VOverlay components aren't display on top of Applet</li>
+   <li><a href="http://dev.vaadin.com/ticket/9148">#9148</a>: Oracle TIMESTAMP not supported in SQLContainer</li>
+   <li><a href="http://dev.vaadin.com/ticket/9189">#9189</a>: Variables sent to the DragAndDropManager are not logged in the debug console</li>
+   <li><a href="http://dev.vaadin.com/ticket/9128">#9128</a>: Typo in vaadin book</li>
+   <li><a href="http://dev.vaadin.com/ticket/8736">#8736</a>: com.vaadin.ui.Table component return null value on multiple selection</li>
+   <li><a href="http://dev.vaadin.com/ticket/9132">#9132</a>: No entries in Table with a defined pageLength causes Table to shrink in height</li>
+   <li><a href="http://dev.vaadin.com/ticket/9147">#9147</a>: SQLContainer Filters - Comparison against NULL value produces error</li>
+   <li><a href="http://dev.vaadin.com/ticket/9154">#9154</a>: NumberFormatException in client-side component VSplitPanel</li>
+   <li><a href="http://dev.vaadin.com/ticket/9171">#9171</a>: StreamVariable never cleaned up in CommunicationManager</li>
+   <li><a href="http://dev.vaadin.com/ticket/9187">#9187</a>: No horizontal scroll bar on empty Table</li>
+   <li><a href="http://dev.vaadin.com/ticket/8838">#8838</a>: Drag and Drop in Google Chrome causes ArrayIndexOutOfBoundsException in AbstractCommunicationManager#convertMap (1612)</li>
+   <li><a href="http://dev.vaadin.com/ticket/8917">#8917</a>: input prompt text is leaking into the component's value</li>
+       </ul>
+           
+       <p>
+         The <a
+         href="http://dev.vaadin.com/query?status=closed&resolution=fixed&milestone=Vaadin+@version@&order=priority">full
+         list of the closed issues</a> can be found at <tt>dev.vaadin.com</tt>.
+       </p>
++>>>>>>> remotes/origin/6.8
  
 -      <!-- ================================================================ -->
 -      <h3 id="enhancements">Enhancements in Vaadin @version-minor@</h3>
 +      <h2 id="enhancements">Enhancements in Vaadin @version-minor@</h2>
  
        <p>
 -        Below is a list of enchacements in the current minor release branch, first
 -        released in @version-minor@.0.
 +        @version-minor@ is the third development release of the upcoming Version 7 of the
 +        Vaadin Framework. It introduces the third set of new features in Vaadin 7, for the
 +        purpose of receiving feedback about the changes.
        </p>
  
 +      <p>The major changes in this third alpha phase are:</p>
 +
        <ul>
 -        <li>
 -          Native scrolling support for Android and iOS (<a href="http://dev.vaadin.com/ticket/8763">#8763</a>)
 +        <li>GWT is now built into Vaadin Framework
            <ul>
 -            <li>
 -              Non-native scrolling implementation used in iOS 5 because of an iOS bug (see <a href="http://dev.vaadin.com/ticket/8792">#8792</a>)
 -            </li>
 +            <li>New SuperDevMode allows debugging client-side Java code in the browser without any plugins</li>
            </ul>
          </li>
 -        <li>
 -          Possibility to fire <b>Button</b> click events on the server-side (<a href="http://dev.vaadin.com/ticket/8209">#8209</a>)
 -        </li>
 -        <li>
 -          Possibility to use HTML inside a <b>Button</b> caption (<a href="http://dev.vaadin.com/ticket/8663">#8663</a>)
 -        </li>
 -        <li>
 -          Possibility to set "alternative text" (<tt>alt</tt> attribute) for the <b>Embedded</b> component (<a href="http://dev.vaadin.com/ticket/6085">#6085</a>)
 -        </li>
 -        <li>
 -          Possibility to query the browser window width and height on the server-side (<a href="http://dev.vaadin.com/ticket/5655">#5655</a>)
 -        </li>
 -        <li>
 -          Keyboard navigation in <b>TabSheet</b>  (<a href="http://dev.vaadin.com/ticket/5100">#5100</a>)
 -        </li>
 -        <li>
 -          Max/min limits for splitter position in <b>SplitPanel</b> (<a href="http://dev.vaadin.com/ticket/1744">#1744</a>)
 -        </li>
 -        <li>
 -          Extended day range in month view to six full weeks in <b>DateField</b> (<a href="http://dev.vaadin.com/ticket/6718">#6718</a>)
 -        </li>
 -        <li>
 -          Non-collapsible <b>Table</b> columns (<a href="http://dev.vaadin.com/ticket/7495">#7495</a>)
 -        </li>
 -        <li>
 -          Selecting a <b>TabSheet</b> tab by its position or a <b>Tab</b> instance (<a href="http://dev.vaadin.com/ticket/8203">#8203</a>)
 -        </li>
 -        <li>
 -          Getting a component by its index or the index of a given component in <b>CssLayout</b> (<a href="http://dev.vaadin.com/ticket/7614">#7614</a>)
 -        </li>
 -        <li>
 -          Removing all <b>Validators</b> of a <b>Field</b> at once (<a href="http://dev.vaadin.com/ticket/8307">#8307</a>)
 -        </li>
 -        <li>
 -          Debug IDs unique to a window, not the whole application (<a href="http://dev.vaadin.com/ticket/5109">#5109</a>)
 -        </li>
 -        <li>
 -          Larger default size for the debug window (<a href="http://dev.vaadin.com/ticket/8523">#8523</a>)
 -        </li>
 -        <li>
 -          Compatibility with Google SuperDevMode (<a href="http://dev.vaadin.com/ticket/8924">#8924</a>)
 -        </li>
 -        <li>
 -          An add-on for handling broken classloaders (<a href="http://dev.vaadin.com/ticket/8447">#8447</a>)
 +        <li>Support for JavaScript components - GWT integration code not required</li>
 +        <li>Handle JavaScript callbacks on the server-side</li>
 +        <li>Component and root extensions</li>
 +        <li>Navigation API for view navigation
            <ul>
 -            <li>
 -              Available in Vaadin Directory: <a href="http://vaadin.com/addon/vaadin-application-server-class-loader-workaround">Vaadin-application-server-class-loader-workaround</a>
 -            </li>
 +            <li>View navigation with bookmarking/linking with URI fragments and browser history support</li>
 +            <li>Access control for views, view change confirmation</li>
            </ul>
          </li>
 +        <li>Resource loading (JS/CSS) by the framework</li>
        </ul>
 +        
 +      <p>
 +        The enhancements are described in more detail in the <a
 +        href="https://vaadin.com/wiki/-/wiki/Main/Vaadin+7">mini-tutorials</a> in the
 +        Vaadin Wiki.
 +      </p>
 +
 +      <p>
 +        Please see the release notes for <a
 +        href="http://vaadin.com/download/prerelease/7.0/7.0.0/7.0.0.alpha1/release-notes.html">alpha1</a>
 +        and <a
 +        href="http://vaadin.com/download/prerelease/7.0/7.0.0/7.0.0.alpha2/release-notes.html">alpha2</a>
 +        for a complete list of major enhancements for Vaadin 7 so far.
 +      </p>
  
 -      <!-- ================================================================ -->
 +      <h3 id="changelog">ChangeLog</h3>
  
 -      <!--
 -      <h3 id="backwardsincompatibilities">Backwards incompatible changes in Vaadin
 -      @version-minor@</h3>
 +      <p>
 +        For a complete list of changes in this release, please see the <a
 +        href="http://dev.vaadin.com/query?status=closed&group=resolution&milestone=Vaadin+7.0.0.alpha3">list
 +        of closed tickets</a>.
 +      </p>
  
 -      <p><i>-- To be done --</i></p>
 -      -->
 -      <!--  <h2 id="security-fixes">Security fixes in Vaadin @version-minor@</h2>-->
 +      <h2 id="migrating">Migrating from Vaadin 6</h2>
  
 -      <!-- ================================================================ -->
 -      <h3 id="overview">Package Contents</h3>
 +      <p>
 +        All Vaadin 6 applications need some changes when migrating to Vaadin 7. The most
 +        obvious changes are in the application/window API and require extending either
 +        <b>Root</b> or <b>Root.LegacyApplication</b> instead of <b>Application</b>.
 +      </p>
  
        <p>
 -        Vaadin Framework is distributed as a single JAR file. Inside the JAR you will
 -        find:
 +        Vaadin 6 add-ons (ones that contain widgets) do not work in Vaadin 7 - please
 +        check the add-ons in <a href="http://vaadin.com/directory/">Vaadin Directory</a>
 +        for Vaadin 7 support.
        </p>
  
 -      <ul>
 -        <li>Vaadin server and client side classes (/com)</li>
 -        <li>Vaadin server and client side sources (/com)</li>
 -        <li>The default widget set (/VAADIN/widgetsets)</li>
 -        <li>Themes: Runo, Reindeer and Chameleon (/VAADIN/themes)</li>
 -        <li>Release notes (/release-notes.html)</li>
 -        <li>Licensing information (/license.html)</li>
 -      </ul>
 +      <p>
 +        Any custom client-side widgets need to be changed to use the new client-server
 +        communication API or the Vaadin 6 compatibility API.
 +      </p>
  
 -      <!-- ================================================================ -->
 -      <h3 id="dependencies">Vaadin @version@ dependencies</h3>
 +      <p>
 +        A detailed list of migration changes are given in the <a
 +        href="http://dev.vaadin.com/wiki/Vaadin7/MigrationGuide">Vaadin 7 Migration
 +        Guide</a>.
 +      </p>
 +
 +      <h2 id="dependencies">Vaadin @version@ Dependencies</h2>
 +
 +      <h3>Bean Validation</h3>
  
        <p>
 -              Vaadin uses GWT @gwt-version@ for widget set compilation. GWT can be
 -              downloaded from <a href="http://code.google.com/webtoolkit/">http://code.google.com/webtoolkit/</a>.
 -              GWT can also be automatically downloaded by the Vaadin Plug-in for
 -              Eclipse. Please note that GWT @gwt-version@ requires the <i>validation-api-1.0.0.GA.jar</i>
 -              and <i>validation-api-1.0.0.GA-sources.jar</i> files in addition to <i>gwt-dev.jar</i>
 -              and <i>gwt-user.jar</i> for widget set compilation.
 +        If you want to use the bean validation feature in Vaadin 7, you need a Bean
 +        Validation API implementation. You need to install the JAR in the
 +        <tt>WEB-INF/lib</tt> directory.
        </p>
  
 -      <!-- ================================================================ -->
        <h2 id="upgrading">Upgrading to Vaadin @version-minor@</h2>
  
 +      <h3>Upgrading the Eclipse Plugin</h3>
 +
        <p>
 -        When upgrading from an earlier Vaadin version, you must:
 +        This release requires that you use the <i>experimental</i> Vaadin Plugin for
 +        Eclipse. Its update site is
 +        <tt>http://vaadin.com/eclipse/experimental</tt>. Please see the <a
 +        href="http://vaadin.com/eclipse">installation instructions</a> for more details.
        </p>
  
 +      <h3>General Upgrading Instructions</h3>
 +
 +      <p>
 +        When upgrading from an earlier Vaadin version, you must:
 +      </p>
 +              
        <ul>
          <li>Recompile your classes using the new Vaadin JAR. Binary
            compatibility is only guaranteed for maintenance releases of
        </p>
  
        <ul>
++<<<<<<< HEAD
 +        <li>Mozilla Firefox 12</li>
 +        <li>Internet Explorer 8-9</li>
 +        <li>Safari 5</li>
 +        <li>Opera 11</li>
 +        <li>Google Chrome 19</li>
++=======
+         <li>Mozilla Firefox 3-14</li>
+         <li>Internet Explorer 6-9</li>
+         <li>Safari 4-5</li>
+         <li>Opera 10-12</li>
+         <li>Google Chrome 13-21</li>
++>>>>>>> remotes/origin/6.8
        </ul>
  
        <p>
Simple merge
diff --cc build/build.xml
index ba9b93a2ab2a97c43015d17e6aeb2e9917e41a2c,4f267c333d05d6401a41d964fa0e02de96b9c866..2988218c4de8bfbdbb71e8704d612047c97d8dfe
          <copy todir="${output-dir}/WebContent">
              <fileset dir="WebContent">
                  <exclude name="**/.svn" />
 -                <!-- TODO check what is necessary -->
                  <include name="WEB-INF/lib/hsqldb.jar" />
                  <include name="VAADIN/themes/**/*" />
 +              <include name="VAADIN/vaadinBootstrap.js" />
                  <include name="META-INF/**/*" />
+               <include name="statictestfiles/**" />
              </fileset>
          </copy>
  
index de2d9a9cd8a9f1bd94dd1622128b213c3a2f5cb4,0000000000000000000000000000000000000000..680131c70ceebf9ff183d0f593d6eb5a8178c548
mode 100644,000000..100644
--- /dev/null
@@@ -1,390 -1,0 +1,435 @@@
-                         + browserDetails.getBrowserMajorVersion();
 +/* 
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +
 +package com.vaadin.terminal.gwt.client;
 +
 +import com.google.gwt.user.client.ui.RootPanel;
 +import com.vaadin.shared.VBrowserDetails;
 +
 +/**
 + * Class used to query information about web browser.
 + * 
 + * Browser details are detected only once and those are stored in this singleton
 + * class.
 + * 
 + */
 +public class BrowserInfo {
 +
 +    private static final String BROWSER_OPERA = "op";
 +    private static final String BROWSER_IE = "ie";
 +    private static final String BROWSER_FIREFOX = "ff";
 +    private static final String BROWSER_SAFARI = "sa";
 +
 +    public static final String ENGINE_GECKO = "gecko";
 +    public static final String ENGINE_WEBKIT = "webkit";
 +    public static final String ENGINE_PRESTO = "presto";
 +    public static final String ENGINE_TRIDENT = "trident";
 +
 +    private static final String OS_WINDOWS = "win";
 +    private static final String OS_LINUX = "lin";
 +    private static final String OS_MACOSX = "mac";
 +    private static final String OS_ANDROID = "android";
 +    private static final String OS_IOS = "ios";
 +
 +    // Common CSS class for all touch devices
 +    private static final String UI_TOUCH = "touch";
 +
 +    private static BrowserInfo instance;
 +
 +    private static String cssClass = null;
 +
 +    static {
 +        // Add browser dependent v-* classnames to body to help css hacks
 +        String browserClassnames = get().getCSSClass();
 +        RootPanel.get().addStyleName(browserClassnames);
 +    }
 +
 +    /**
 +     * Singleton method to get BrowserInfo object.
 +     * 
 +     * @return instance of BrowserInfo object
 +     */
 +    public static BrowserInfo get() {
 +        if (instance == null) {
 +            instance = new BrowserInfo();
 +        }
 +        return instance;
 +    }
 +
 +    private VBrowserDetails browserDetails;
 +    private boolean touchDevice;
 +
 +    private BrowserInfo() {
 +        browserDetails = new VBrowserDetails(getBrowserString());
 +        if (browserDetails.isIE()) {
 +            // Use document mode instead user agent to accurately detect how we
 +            // are rendering
 +            int documentMode = getIEDocumentMode();
 +            if (documentMode != -1) {
 +                browserDetails.setIEMode(documentMode);
 +            }
 +        }
 +
 +        if (browserDetails.isChrome()) {
 +            touchDevice = detectChromeTouchDevice();
 +        } else {
 +            touchDevice = detectTouchDevice();
 +        }
 +    }
 +
 +    private native boolean detectTouchDevice()
 +    /*-{
 +        try { document.createEvent("TouchEvent");return true;} catch(e){return false;};
 +    }-*/;
 +
 +    private native boolean detectChromeTouchDevice()
 +    /*-{
 +        return ("ontouchstart" in window);
 +    }-*/;
 +
 +    private native int getIEDocumentMode()
 +    /*-{
 +      var mode = $wnd.document.documentMode;
 +      if (!mode)
 +               return -1;
 +      return mode;
 +    }-*/;
 +
 +    /**
 +     * Returns a string representing the browser in use, for use in CSS
 +     * classnames. The classnames will be space separated abbreviations,
 +     * optionally with a version appended.
 +     * 
 +     * Abbreviations: Firefox: ff Internet Explorer: ie Safari: sa Opera: op
 +     * 
 +     * Browsers that CSS-wise behave like each other will get the same
 +     * abbreviation (this usually depends on the rendering engine).
 +     * 
 +     * This is quite simple at the moment, more heuristics will be added when
 +     * needed.
 +     * 
 +     * Examples: Internet Explorer 6: ".v-ie .v-ie6 .v-ie60", Firefox 3.0.4:
 +     * ".v-ff .v-ff3 .v-ff30", Opera 9.60: ".v-op .v-op9 .v-op960", Opera 10.10:
 +     * ".v-op .v-op10 .v-op1010"
 +     * 
 +     * @return
 +     */
 +    public String getCSSClass() {
 +        String prefix = "v-";
 +
 +        if (cssClass == null) {
 +            String browserIdentifier = "";
 +            String majorVersionClass = "";
 +            String minorVersionClass = "";
 +            String browserEngineClass = "";
 +
 +            if (browserDetails.isFirefox()) {
 +                browserIdentifier = BROWSER_FIREFOX;
 +                majorVersionClass = browserIdentifier
-                         + browserDetails.getBrowserMajorVersion();
++                        + getBrowserMajorVersion();
 +                minorVersionClass = majorVersionClass
 +                        + browserDetails.getBrowserMinorVersion();
 +                browserEngineClass = ENGINE_GECKO;
 +            } else if (browserDetails.isChrome()) {
 +                // TODO update when Chrome is more stable
 +                browserIdentifier = BROWSER_SAFARI;
 +                majorVersionClass = "ch";
 +                browserEngineClass = ENGINE_WEBKIT;
 +            } else if (browserDetails.isSafari()) {
 +                browserIdentifier = BROWSER_SAFARI;
 +                majorVersionClass = browserIdentifier
-                         + browserDetails.getBrowserMajorVersion();
++                        + getBrowserMajorVersion();
 +                minorVersionClass = majorVersionClass
 +                        + browserDetails.getBrowserMinorVersion();
 +                browserEngineClass = ENGINE_WEBKIT;
 +            } else if (browserDetails.isIE()) {
 +                browserIdentifier = BROWSER_IE;
 +                majorVersionClass = browserIdentifier
-                         + browserDetails.getBrowserMajorVersion();
++                        + getBrowserMajorVersion();
 +                minorVersionClass = majorVersionClass
 +                        + browserDetails.getBrowserMinorVersion();
 +                browserEngineClass = ENGINE_TRIDENT;
 +            } else if (browserDetails.isOpera()) {
 +                browserIdentifier = BROWSER_OPERA;
 +                majorVersionClass = browserIdentifier
-         return isIE() && browserDetails.getBrowserMajorVersion() == 8;
++                        + getBrowserMajorVersion();
 +                minorVersionClass = majorVersionClass
 +                        + browserDetails.getBrowserMinorVersion();
 +                browserEngineClass = ENGINE_PRESTO;
 +            }
 +
 +            cssClass = prefix + browserIdentifier;
 +            if (!"".equals(majorVersionClass)) {
 +                cssClass = cssClass + " " + prefix + majorVersionClass;
 +            }
 +            if (!"".equals(minorVersionClass)) {
 +                cssClass = cssClass + " " + prefix + minorVersionClass;
 +            }
 +            if (!"".equals(browserEngineClass)) {
 +                cssClass = cssClass + " " + prefix + browserEngineClass;
 +            }
 +            String osClass = getOperatingSystemClass();
 +            if (osClass != null) {
 +                cssClass = cssClass + " " + prefix + osClass;
 +            }
 +            if (isTouchDevice()) {
 +                cssClass = cssClass + " " + prefix + UI_TOUCH;
 +            }
 +        }
 +
 +        return cssClass;
 +    }
 +
 +    private String getOperatingSystemClass() {
 +        if (browserDetails.isAndroid()) {
 +            return OS_ANDROID;
 +        } else if (browserDetails.isIOS()) {
 +            return OS_IOS;
 +        } else if (browserDetails.isWindows()) {
 +            return OS_WINDOWS;
 +        } else if (browserDetails.isLinux()) {
 +            return OS_LINUX;
 +        } else if (browserDetails.isMacOSX()) {
 +            return OS_MACOSX;
 +        }
 +        // Unknown OS
 +        return null;
 +    }
 +
 +    public boolean isIE() {
 +        return browserDetails.isIE();
 +    }
 +
 +    public boolean isFirefox() {
 +        return browserDetails.isFirefox();
 +    }
 +
 +    public boolean isSafari() {
 +        return browserDetails.isSafari();
 +    }
 +
 +    public boolean isIE8() {
-         return isIE() && browserDetails.getBrowserMajorVersion() == 9;
++        return isIE() && getBrowserMajorVersion() == 8;
 +    }
 +
 +    public boolean isIE9() {
-         return browserDetails.getBrowserMajorVersion();
++        return isIE() && getBrowserMajorVersion() == 9;
 +    }
 +
 +    public boolean isChrome() {
 +        return browserDetails.isChrome();
 +    }
 +
 +    public boolean isGecko() {
 +        return browserDetails.isGecko();
 +    }
 +
 +    public boolean isWebkit() {
 +        return browserDetails.isWebKit();
 +    }
 +
 +    /**
 +     * Returns the Gecko version if the browser is Gecko based. The Gecko
 +     * version for Firefox 2 is 1.8 and 1.9 for Firefox 3.
 +     * 
 +     * @return The Gecko version or -1 if the browser is not Gecko based
 +     */
 +    public float getGeckoVersion() {
 +        if (!browserDetails.isGecko()) {
 +            return -1;
 +        }
 +
 +        return browserDetails.getBrowserEngineVersion();
 +    }
 +
 +    /**
 +     * Returns the WebKit version if the browser is WebKit based. The WebKit
 +     * version returned is the major version e.g., 523.
 +     * 
 +     * @return The WebKit version or -1 if the browser is not WebKit based
 +     */
 +    public float getWebkitVersion() {
 +        if (!browserDetails.isWebKit()) {
 +            return -1;
 +        }
 +
 +        return browserDetails.getBrowserEngineVersion();
 +    }
 +
 +    public float getIEVersion() {
 +        if (!browserDetails.isIE()) {
 +            return -1;
 +        }
 +
-         return browserDetails.getBrowserMajorVersion();
++        return getBrowserMajorVersion();
 +    }
 +
 +    public float getOperaVersion() {
 +        if (!browserDetails.isOpera()) {
 +            return -1;
 +        }
 +
-         return browserDetails.isOpera()
-                 && browserDetails.getBrowserMajorVersion() == 10;
++        return getBrowserMajorVersion();
 +    }
 +
 +    public boolean isOpera() {
 +        return browserDetails.isOpera();
 +    }
 +
 +    public boolean isOpera10() {
-         return browserDetails.isOpera()
-                 && browserDetails.getBrowserMajorVersion() == 11;
++        return browserDetails.isOpera() && getBrowserMajorVersion() == 10;
 +    }
 +
 +    public boolean isOpera11() {
++        return browserDetails.isOpera() && getBrowserMajorVersion() == 11;
 +    }
 +
 +    public native static String getBrowserString()
 +    /*-{
 +      return $wnd.navigator.userAgent;
 +    }-*/;
 +
 +    public native int getScreenWidth()
 +    /*-{
 +      return $wnd.screen.width;
 +    }-*/;
 +
 +    public native int getScreenHeight()
 +    /*-{
 +      return $wnd.screen.height;
 +    }-*/;
 +
 +    /**
 +     * @return true if the browser runs on a touch based device.
 +     */
 +    public boolean isTouchDevice() {
 +        return touchDevice;
 +    }
 +
 +    /**
 +     * Indicates whether the browser might require juggling to properly update
 +     * sizes inside elements with overflow: auto.
 +     * 
 +     * @return <code>true</code> if the browser requires the workaround,
 +     *         otherwise <code>false</code>
 +     */
 +    public boolean requiresOverflowAutoFix() {
 +        return (getWebkitVersion() > 0 || getOperaVersion() >= 11)
 +                && Util.getNativeScrollbarSize() > 0;
 +    }
 +
 +    /**
 +     * Checks if the browser is run on iOS
 +     * 
 +     * @return true if the browser is run on iOS, false otherwise
 +     */
 +    public boolean isIOS() {
 +        return browserDetails.isIOS();
 +    }
 +
 +    /**
 +     * Checks if the browser is run on Android
 +     * 
 +     * @return true if the browser is run on Android, false otherwise
 +     */
 +    public boolean isAndroid() {
 +        return browserDetails.isAndroid();
 +    }
 +
 +    /**
 +     * Checks if the browser is capable of handling scrolling natively or if a
 +     * touch scroll helper is needed for scrolling.
 +     * 
 +     * @return true if browser needs a touch scroll helper, false if the browser
 +     *         can handle scrolling natively
 +     */
 +    public boolean requiresTouchScrollDelegate() {
 +        if (!isTouchDevice()) {
 +            return false;
 +        }
 +        if (isAndroid() && isWebkit() && getWebkitVersion() >= 534) {
 +            return false;
 +        }
 +        // Cannot enable native touch scrolling on iOS 5 until #8792 is resolved
 +        // if (isIOS() && isWebkit() && getWebkitVersion() >= 534) {
 +        // return false;
 +        // }
 +        return true;
 +    }
 +
 +    /**
 +     * Tests if this is an Android devices with a broken scrollTop
 +     * implementation
 +     * 
 +     * @return true if scrollTop cannot be trusted on this device, false
 +     *         otherwise
 +     */
 +    public boolean isAndroidWithBrokenScrollTop() {
 +        return isAndroid()
 +                && (getOperatingSystemMajorVersion() == 3 || getOperatingSystemMajorVersion() == 4);
 +    }
 +
 +    private int getOperatingSystemMajorVersion() {
 +        return browserDetails.getOperatingSystemMajorVersion();
 +    }
++
++    /**
++     * Returns the browser major version e.g., 3 for Firefox 3.5, 4 for Chrome
++     * 4, 8 for Internet Explorer 8.
++     * <p>
++     * Note that Internet Explorer 8 and newer will return the document mode so
++     * IE8 rendering as IE7 will return 7.
++     * </p>
++     * 
++     * @return The major version of the browser.
++     */
++    public int getBrowserMajorVersion() {
++        return browserDetails.getBrowserMajorVersion();
++    }
++
++    /**
++     * Returns the browser minor version e.g., 5 for Firefox 3.5.
++     * 
++     * @see #getBrowserMajorVersion()
++     * 
++     * @return The minor version of the browser, or -1 if not known/parsed.
++     */
++    public int getBrowserMinorVersion() {
++        return browserDetails.getBrowserMinorVersion();
++    }
++
++    /**
++     * Checks if the browser version is newer or equal to the given major+minor
++     * version.
++     * 
++     * @param majorVersion
++     *            The major version to check for
++     * @param minorVersion
++     *            The minor version to check for
++     * @return true if the browser version is newer or equal to the given
++     *         version
++     */
++    public boolean isBrowserVersionNewerOrEqual(int majorVersion,
++            int minorVersion) {
++        if (getBrowserMajorVersion() == majorVersion) {
++            // Same major
++            return (getBrowserMinorVersion() >= minorVersion);
++        }
++
++        // Older or newer major
++        return (getBrowserMajorVersion() > majorVersion);
++    }
 +}
index aef21ac737aba36364b71a486b28f0880c36f295,0000000000000000000000000000000000000000..97201de297d3e32181998f2881773bab8e611a9c
mode 100644,000000..100644
--- /dev/null
@@@ -1,567 -1,0 +1,595 @@@
-     private void removeShim() {
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui;
 +
 +import com.google.gwt.animation.client.Animation;
 +import com.google.gwt.dom.client.Document;
 +import com.google.gwt.dom.client.IFrameElement;
 +import com.google.gwt.dom.client.Style;
 +import com.google.gwt.dom.client.Style.BorderStyle;
 +import com.google.gwt.dom.client.Style.Position;
 +import com.google.gwt.dom.client.Style.Unit;
 +import com.google.gwt.event.logical.shared.CloseEvent;
 +import com.google.gwt.event.logical.shared.CloseHandler;
 +import com.google.gwt.user.client.DOM;
 +import com.google.gwt.user.client.Element;
 +import com.google.gwt.user.client.ui.PopupPanel;
 +import com.google.gwt.user.client.ui.RootPanel;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.terminal.gwt.client.BrowserInfo;
 +
 +/**
 + * In Vaadin UI this Overlay should always be used for all elements that
 + * temporary float over other components like context menus etc. This is to deal
 + * stacking order correctly with VWindow objects.
 + */
 +public class VOverlay extends PopupPanel implements CloseHandler<PopupPanel> {
 +
 +    public static class PositionAndSize {
 +        private int left, top, width, height;
 +
++        public PositionAndSize(int left, int top, int width, int height) {
++            super();
++            setLeft(left);
++            setTop(top);
++            setWidth(width);
++            setHeight(height);
++        }
++
 +        public int getLeft() {
 +            return left;
 +        }
 +
 +        public void setLeft(int left) {
 +            this.left = left;
 +        }
 +
 +        public int getTop() {
 +            return top;
 +        }
 +
 +        public void setTop(int top) {
 +            this.top = top;
 +        }
 +
 +        public int getWidth() {
 +            return width;
 +        }
 +
 +        public void setWidth(int width) {
++            if (width < 0) {
++                width = 0;
++            }
++
 +            this.width = width;
 +        }
 +
 +        public int getHeight() {
 +            return height;
 +        }
 +
 +        public void setHeight(int height) {
++            if (height < 0) {
++                height = 0;
++            }
++
 +            this.height = height;
 +        }
 +
 +        public void setAnimationFromCenterProgress(double progress) {
 +            left += (int) (width * (1.0 - progress) / 2.0);
 +            top += (int) (height * (1.0 - progress) / 2.0);
 +            width = (int) (width * progress);
 +            height = (int) (height * progress);
 +        }
 +    }
 +
 +    /*
 +     * The z-index value from where all overlays live. This can be overridden in
 +     * any extending class.
 +     */
 +    public static int Z_INDEX = 20000;
 +
 +    private static int leftFix = -1;
 +
 +    private static int topFix = -1;
 +
 +    /*
 +     * Shadow element style. If an extending class wishes to use a different
 +     * style of shadow, it can use setShadowStyle(String) to give the shadow
 +     * element a new style name.
 +     */
 +    public static final String CLASSNAME_SHADOW = "v-shadow";
 +
 +    /*
 +     * The shadow element for this overlay.
 +     */
 +    private Element shadow;
 +
 +    /*
 +     * Creator of VOverlow (widget that made the instance, not the layout
 +     * parent)
 +     */
 +    private Widget owner;
 +
 +    /**
 +     * The shim iframe behind the overlay, allowing PDFs and applets to be
 +     * covered by overlays.
 +     */
 +    private IFrameElement shimElement;
 +
 +    /**
 +     * The HTML snippet that is used to render the actual shadow. In consists of
 +     * nine different DIV-elements with the following class names:
 +     * 
 +     * <pre>
 +     *   .v-shadow[-stylename]
 +     *   ----------------------------------------------
 +     *   | .top-left     |   .top    |     .top-right |
 +     *   |---------------|-----------|----------------|
 +     *   |               |           |                |
 +     *   | .left         |  .center  |         .right |
 +     *   |               |           |                |
 +     *   |---------------|-----------|----------------|
 +     *   | .bottom-left  |  .bottom  |  .bottom-right |
 +     *   ----------------------------------------------
 +     * </pre>
 +     * 
 +     * See default theme 'shadow.css' for implementation example.
 +     */
 +    private static final String SHADOW_HTML = "<div class=\"top-left\"></div><div class=\"top\"></div><div class=\"top-right\"></div><div class=\"left\"></div><div class=\"center\"></div><div class=\"right\"></div><div class=\"bottom-left\"></div><div class=\"bottom\"></div><div class=\"bottom-right\"></div>";
 +
 +    /**
 +     * Matches {@link PopupPanel}.ANIMATION_DURATION
 +     */
 +    private static final int POPUP_PANEL_ANIMATION_DURATION = 200;
 +
 +    private boolean sinkShadowEvents = false;
 +
 +    public VOverlay() {
 +        super();
 +        adjustZIndex();
 +    }
 +
 +    public VOverlay(boolean autoHide) {
 +        super(autoHide);
 +        adjustZIndex();
 +    }
 +
 +    public VOverlay(boolean autoHide, boolean modal) {
 +        super(autoHide, modal);
 +        adjustZIndex();
 +    }
 +
 +    public VOverlay(boolean autoHide, boolean modal, boolean showShadow) {
 +        super(autoHide, modal);
 +        setShadowEnabled(showShadow);
 +        adjustZIndex();
 +    }
 +
 +    /**
 +     * Method to controle whether DOM elements for shadow are added. With this
 +     * method subclasses can control displaying of shadow also after the
 +     * constructor.
 +     * 
 +     * @param enabled
 +     *            true if shadow should be displayed
 +     */
 +    protected void setShadowEnabled(boolean enabled) {
 +        if (enabled != isShadowEnabled()) {
 +            if (enabled) {
 +                shadow = DOM.createDiv();
 +                shadow.setClassName(CLASSNAME_SHADOW);
 +                shadow.setInnerHTML(SHADOW_HTML);
 +                DOM.setStyleAttribute(shadow, "position", "absolute");
 +                addCloseHandler(this);
 +            } else {
 +                removeShadowIfPresent();
 +                shadow = null;
 +            }
 +        }
 +    }
 +
 +    protected boolean isShadowEnabled() {
 +        return shadow != null;
 +    }
 +
-     private boolean isShimAttached() {
++    private void removeShimElement() {
 +        if (shimElement != null) {
 +            shimElement.removeFromParent();
 +        }
 +    }
 +
 +    private void removeShadowIfPresent() {
 +        if (isShadowAttached()) {
 +            shadow.removeFromParent();
 +
 +            // Remove event listener from the shadow
 +            unsinkShadowEvents();
 +        }
 +    }
 +
 +    private boolean isShadowAttached() {
 +        return isShadowEnabled() && shadow.getParentElement() != null;
 +    }
 +
-         sizeOrPositionUpdated(isAnimationEnabled() ? 0 : 1);
++    private boolean isShimElementAttached() {
 +        return shimElement != null && shimElement.hasParentElement();
 +    }
 +
 +    private void adjustZIndex() {
 +        setZIndex(Z_INDEX);
 +    }
 +
 +    /**
 +     * Set the z-index (visual stack position) for this overlay.
 +     * 
 +     * @param zIndex
 +     *            The new z-index
 +     */
 +    protected void setZIndex(int zIndex) {
 +        DOM.setStyleAttribute(getElement(), "zIndex", "" + zIndex);
 +        if (isShadowEnabled()) {
 +            DOM.setStyleAttribute(shadow, "zIndex", "" + zIndex);
 +        }
 +    }
 +
 +    @Override
 +    public void setPopupPosition(int left, int top) {
 +        // TODO, this should in fact be part of
 +        // Document.get().getBodyOffsetLeft/Top(). Would require overriding DOM
 +        // for all permutations. Now adding fix as margin instead of fixing
 +        // left/top because parent class saves the position.
 +        Style style = getElement().getStyle();
 +        style.setMarginLeft(-adjustByRelativeLeftBodyMargin(), Unit.PX);
 +        style.setMarginTop(-adjustByRelativeTopBodyMargin(), Unit.PX);
 +        super.setPopupPosition(left, top);
-         if (shimElement == null) {
++        positionOrSizeUpdated(isAnimationEnabled() ? 0 : 1);
 +    }
 +
 +    private IFrameElement getShimElement() {
-             sizeOrPositionUpdated(1.0);
++        if (shimElement == null && needsShimElement()) {
 +            shimElement = Document.get().createIFrameElement();
 +
 +            // Insert shim iframe before the main overlay element. It does not
 +            // matter if it is in front or behind the shadow as we cannot put a
 +            // shim behind the shadow due to its transparency.
 +            shimElement.getStyle().setPosition(Position.ABSOLUTE);
 +            shimElement.getStyle().setBorderStyle(BorderStyle.NONE);
 +            shimElement.setTabIndex(-1);
 +            shimElement.setFrameBorder(0);
 +            shimElement.setMarginHeight(0);
 +        }
 +        return shimElement;
 +    }
 +
 +    private int getActualTop() {
 +        int y = getAbsoluteTop();
 +
 +        /* This is needed for IE7 at least */
 +        // Account for the difference between absolute position and the
 +        // body's positioning context.
 +        y -= Document.get().getBodyOffsetTop();
 +        y -= adjustByRelativeTopBodyMargin();
 +
 +        return y;
 +    }
 +
 +    private int getActualLeft() {
 +        int x = getAbsoluteLeft();
 +
 +        /* This is needed for IE7 at least */
 +        // Account for the difference between absolute position and the
 +        // body's positioning context.
 +        x -= Document.get().getBodyOffsetLeft();
 +        x -= adjustByRelativeLeftBodyMargin();
 +
 +        return x;
 +    }
 +
 +    private static int adjustByRelativeTopBodyMargin() {
 +        if (topFix == -1) {
 +            topFix = detectRelativeBodyFixes("top");
 +        }
 +        return topFix;
 +    }
 +
 +    private native static int detectRelativeBodyFixes(String axis)
 +    /*-{
 +        try {
 +            var b = $wnd.document.body;
 +            var cstyle = b.currentStyle ? b.currentStyle : getComputedStyle(b);
 +            if(cstyle && cstyle.position == 'relative') {
 +                return b.getBoundingClientRect()[axis];
 +            }
 +        } catch(e){}
 +        return 0;
 +    }-*/;
 +
 +    private static int adjustByRelativeLeftBodyMargin() {
 +        if (leftFix == -1) {
 +            leftFix = detectRelativeBodyFixes("left");
 +
 +        }
 +        return leftFix;
 +    }
 +
 +    @Override
 +    public void show() {
 +        super.show();
 +        if (isAnimationEnabled()) {
 +            new ResizeAnimation().run(POPUP_PANEL_ANIMATION_DURATION);
 +        } else {
-         removeShim();
++            positionOrSizeUpdated(1.0);
 +        }
 +    }
 +
 +    @Override
 +    protected void onDetach() {
 +        super.onDetach();
 +
 +        // Always ensure shadow is removed when the overlay is removed.
 +        removeShadowIfPresent();
-         sizeOrPositionUpdated(1.0);
++        removeShimElement();
 +    }
 +
 +    @Override
 +    public void setVisible(boolean visible) {
 +        super.setVisible(visible);
 +        if (isShadowEnabled()) {
 +            shadow.getStyle().setProperty("visibility",
 +                    visible ? "visible" : "hidden");
 +        }
 +    }
 +
 +    @Override
 +    public void setWidth(String width) {
 +        super.setWidth(width);
-         sizeOrPositionUpdated(1.0);
++        positionOrSizeUpdated(1.0);
 +    }
 +
 +    @Override
 +    public void setHeight(String height) {
 +        super.setHeight(height);
-     public void sizeOrPositionUpdated() {
-         sizeOrPositionUpdated(1.0);
++        positionOrSizeUpdated(1.0);
 +    }
 +
 +    /**
 +     * Sets the shadow style for this overlay. Will override any previous style
 +     * for the shadow. The default style name is defined by CLASSNAME_SHADOW.
 +     * The given style will be prefixed with CLASSNAME_SHADOW.
 +     * 
 +     * @param style
 +     *            The new style name for the shadow element. Will be prefixed by
 +     *            CLASSNAME_SHADOW, e.g. style=='foobar' -> actual style
 +     *            name=='v-shadow-foobar'.
 +     */
 +    protected void setShadowStyle(String style) {
 +        if (isShadowEnabled()) {
 +            shadow.setClassName(CLASSNAME_SHADOW + "-" + style);
 +        }
 +    }
 +
 +    /**
 +     * Extending classes should always call this method after they change the
 +     * size of overlay without using normal 'setWidth(String)' and
 +     * 'setHeight(String)' methods (if not calling super.setWidth/Height).
 +     * 
 +     */
-     private void sizeOrPositionUpdated(final double progress) {
++    public void positionOrSizeUpdated() {
++        positionOrSizeUpdated(1.0);
++    }
++
++    /**
++     * @deprecated Call {@link #positionOrSizeUpdated()} instead.
++     */
++    @Deprecated
++    protected void updateShadowSizeAndPosition() {
++        positionOrSizeUpdated();
 +    }
 +
 +    /**
 +     * Recalculates proper position and dimensions for the shadow and shim
 +     * elements. Can be used to animate the related elements, using the
 +     * 'progress' parameter (used to animate the shadow in sync with GWT
 +     * PopupPanel's default animation 'PopupPanel.AnimationType.CENTER').
 +     * 
 +     * @param progress
 +     *            A value between 0.0 and 1.0, indicating the progress of the
 +     *            animation (0=start, 1=end).
 +     */
-         PositionAndSize positionAndSize = new PositionAndSize();
-         positionAndSize.left = getActualLeft();
-         positionAndSize.top = getActualTop();
-         positionAndSize.width = getOffsetWidth();
-         positionAndSize.height = getOffsetHeight();
-         if (positionAndSize.width < 0) {
-             positionAndSize.width = 0;
-         }
-         if (positionAndSize.height < 0) {
-             positionAndSize.height = 0;
-         }
++    private void positionOrSizeUpdated(final double progress) {
 +        // Don't do anything if overlay element is not attached
 +        if (!isAttached()) {
 +            return;
 +        }
 +        // Calculate proper z-index
 +        String zIndex = null;
 +        try {
 +            // Odd behaviour with Windows Hosted Mode forces us to use
 +            // this redundant try/catch block (See dev.vaadin.com #2011)
 +            zIndex = DOM.getStyleAttribute(getElement(), "zIndex");
 +        } catch (Exception ignore) {
 +            // Ignored, will cause no harm
 +            zIndex = "1000";
 +        }
 +        if (zIndex == null) {
 +            zIndex = "" + Z_INDEX;
 +        }
 +        // Calculate position and size
 +        if (BrowserInfo.get().isIE()) {
 +            // Shake IE
 +            getOffsetHeight();
 +            getOffsetWidth();
 +        }
 +
-             updateSizeAndPosition(shadow, positionAndSize);
++        PositionAndSize positionAndSize = new PositionAndSize(getActualLeft(),
++                getActualTop(), getOffsetWidth(), getOffsetHeight());
 +
 +        // Animate the size
 +        positionAndSize.setAnimationFromCenterProgress(progress);
 +
 +        // Opera needs some shaking to get parts of the shadow showing
 +        // properly
 +        // (ticket #2704)
 +        if (BrowserInfo.get().isOpera() && isShadowEnabled()) {
 +            // Clear the height of all middle elements
 +            DOM.getChild(shadow, 3).getStyle().setProperty("height", "auto");
 +            DOM.getChild(shadow, 4).getStyle().setProperty("height", "auto");
 +            DOM.getChild(shadow, 5).getStyle().setProperty("height", "auto");
 +        }
 +
 +        // Update correct values
 +        if (isShadowEnabled()) {
-         updateSizeAndPosition((Element) Element.as(getShimElement()),
-                 positionAndSize);
++            updatePositionAndSize(shadow, positionAndSize);
 +            DOM.setStyleAttribute(shadow, "zIndex", zIndex);
 +            DOM.setStyleAttribute(shadow, "display", progress < 0.9 ? "none"
 +                    : "");
 +        }
-                     .getStyle()
-                     .setPropertyPx("height",
-                             DOM.getChild(shadow, 3).getOffsetHeight());
++        if (needsShimElement()) {
++            updatePositionAndSize((Element) Element.as(getShimElement()),
++                    positionAndSize);
++        }
 +
 +        // Opera fix, part 2 (ticket #2704)
 +        if (BrowserInfo.get().isOpera() && isShadowEnabled()) {
 +            // We'll fix the height of all the middle elements
 +            DOM.getChild(shadow, 3)
-                     .getStyle()
-                     .setPropertyPx("height",
-                             DOM.getChild(shadow, 4).getOffsetHeight());
++            .getStyle()
++            .setPropertyPx("height",
++                    DOM.getChild(shadow, 3).getOffsetHeight());
 +            DOM.getChild(shadow, 4)
-                     .getStyle()
-                     .setPropertyPx("height",
-                             DOM.getChild(shadow, 5).getOffsetHeight());
++            .getStyle()
++            .setPropertyPx("height",
++                    DOM.getChild(shadow, 4).getOffsetHeight());
 +            DOM.getChild(shadow, 5)
-         if (!isShimAttached()) {
++            .getStyle()
++            .setPropertyPx("height",
++                    DOM.getChild(shadow, 5).getOffsetHeight());
 +        }
 +
 +        // Attach to dom if not there already
 +        if (isShadowEnabled() && !isShadowAttached()) {
 +            RootPanel.get().getElement().insertBefore(shadow, getElement());
 +            sinkShadowEvents();
 +        }
-                     .insertBefore(shimElement, getElement());
++        if (needsShimElement() && !isShimElementAttached()) {
 +            RootPanel.get().getElement()
-     private void updateSizeAndPosition(Element e,
++            .insertBefore(getShimElement(), getElement());
 +        }
 +
 +    }
 +
-         e.getStyle().setLeft(positionAndSize.left, Unit.PX);
-         e.getStyle().setTop(positionAndSize.top, Unit.PX);
-         e.getStyle().setWidth(positionAndSize.width, Unit.PX);
-         e.getStyle().setHeight(positionAndSize.height, Unit.PX);
++    /**
++     * Returns true if we should add a shim iframe below the overlay to deal
++     * with zindex issues with PDFs and applets. Can be overriden to disable
++     * shim iframes if they are not needed.
++     * 
++     * @return true if a shim iframe should be added, false otherwise
++     */
++    protected boolean needsShimElement() {
++        BrowserInfo info = BrowserInfo.get();
++        return info.isIE() && info.isBrowserVersionNewerOrEqual(8, 0);
++    }
++
++    private void updatePositionAndSize(Element e,
 +            PositionAndSize positionAndSize) {
-             sizeOrPositionUpdated(progress);
++        e.getStyle().setLeft(positionAndSize.getLeft(), Unit.PX);
++        e.getStyle().setTop(positionAndSize.getTop(), Unit.PX);
++        e.getStyle().setWidth(positionAndSize.getWidth(), Unit.PX);
++        e.getStyle().setHeight(positionAndSize.getHeight(), Unit.PX);
 +    }
 +
 +    protected class ResizeAnimation extends Animation {
 +        @Override
 +        protected void onUpdate(double progress) {
++            positionOrSizeUpdated(progress);
 +        }
 +    }
 +
 +    @Override
 +    public void onClose(CloseEvent<PopupPanel> event) {
 +        removeShadowIfPresent();
 +    }
 +
 +    @Override
 +    public void sinkEvents(int eventBitsToAdd) {
 +        super.sinkEvents(eventBitsToAdd);
 +        // Also sink events on the shadow if present
 +        sinkShadowEvents();
 +    }
 +
 +    private void sinkShadowEvents() {
 +        if (isSinkShadowEvents() && isShadowAttached()) {
 +            // Sink the same events as the actual overlay has sunk
 +            DOM.sinkEvents(shadow, DOM.getEventsSunk(getElement()));
 +            // Send events to VOverlay.onBrowserEvent
 +            DOM.setEventListener(shadow, this);
 +        }
 +    }
 +
 +    private void unsinkShadowEvents() {
 +        if (isShadowAttached()) {
 +            DOM.setEventListener(shadow, null);
 +            DOM.sinkEvents(shadow, 0);
 +        }
 +    }
 +
 +    /**
 +     * Enables or disables sinking the events of the shadow to the same
 +     * onBrowserEvent as events to the actual overlay goes.
 +     * 
 +     * Please note, that if you enable this, you can't assume that e.g.
 +     * event.getEventTarget returns an element inside the DOM structure of the
 +     * overlay
 +     * 
 +     * @param sinkShadowEvents
 +     */
 +    protected void setSinkShadowEvents(boolean sinkShadowEvents) {
 +        this.sinkShadowEvents = sinkShadowEvents;
 +        if (sinkShadowEvents) {
 +            sinkShadowEvents();
 +        } else {
 +            unsinkShadowEvents();
 +        }
 +    }
 +
 +    protected boolean isSinkShadowEvents() {
 +        return sinkShadowEvents;
 +    }
 +
 +    /**
 +     * Get owner (Widget that made this VOverlay, not the layout parent) of
 +     * VOverlay
 +     * 
 +     * @return Owner (creator) or null if not defined
 +     */
 +    public Widget getOwner() {
 +        return owner;
 +    }
 +
 +    /**
 +     * Set owner (Widget that made this VOverlay, not the layout parent) of
 +     * VOverlay
 +     * 
 +     * @param owner
 +     *            Owner (creator) of VOverlay
 +     */
 +    public void setOwner(Widget owner) {
 +        this.owner = owner;
 +    }
 +}
index 9f17b8169129043e6f85cc215f4d8f5e89935632,0000000000000000000000000000000000000000..fe47fcca663aa2a75752b44c763b7ff93fbbde8a
mode 100644,000000..100644
--- /dev/null
@@@ -1,1463 -1,0 +1,1463 @@@
-         CloseHandler<PopupPanel>, KeyPressHandler, KeyDownHandler,
-         FocusHandler, SubPartAware {
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +package com.vaadin.terminal.gwt.client.ui.menubar;
 +
 +import java.util.ArrayList;
 +import java.util.List;
 +
 +import com.google.gwt.core.client.GWT;
 +import com.google.gwt.core.client.Scheduler;
 +import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 +import com.google.gwt.dom.client.Style;
 +import com.google.gwt.dom.client.Style.Overflow;
 +import com.google.gwt.dom.client.Style.Unit;
 +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.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.ui.HasHTML;
 +import com.google.gwt.user.client.ui.PopupPanel;
 +import com.google.gwt.user.client.ui.RootPanel;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.shared.ui.menubar.MenuBarConstants;
 +import com.vaadin.terminal.gwt.client.ApplicationConnection;
 +import com.vaadin.terminal.gwt.client.BrowserInfo;
 +import com.vaadin.terminal.gwt.client.LayoutManager;
 +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.ui.Icon;
 +import com.vaadin.terminal.gwt.client.ui.SimpleFocusablePanel;
 +import com.vaadin.terminal.gwt.client.ui.SubPartAware;
 +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
 +import com.vaadin.terminal.gwt.client.ui.VOverlay;
 +
 +public class VMenuBar extends SimpleFocusablePanel implements
-                 @Override
-                 public void execute() {
-                     iLayout(true);
-                 }
-             });
++CloseHandler<PopupPanel>, KeyPressHandler, KeyDownHandler,
++FocusHandler, SubPartAware {
 +
 +    // The hierarchy of VMenuBar is a bit weird as VMenuBar is the Paintable,
 +    // used for the root menu but also used for the sub menus.
 +
 +    /** Set the CSS class name to allow styling. */
 +    public static final String CLASSNAME = "v-menubar";
 +
 +    /** For server connections **/
 +    protected String uidlId;
 +    protected ApplicationConnection client;
 +
 +    protected final VMenuBar hostReference = this;
 +    protected CustomMenuItem moreItem = null;
 +
 +    // Only used by the root menu bar
 +    protected VMenuBar collapsedRootItems;
 +
 +    // Construct an empty command to be used when the item has no command
 +    // associated
 +    protected static final Command emptyCommand = null;
 +
 +    /** Widget fields **/
 +    protected boolean subMenu;
 +    protected ArrayList<CustomMenuItem> items;
 +    protected Element containerElement;
 +    protected VOverlay popup;
 +    protected VMenuBar visibleChildMenu;
 +    protected boolean menuVisible = false;
 +    protected VMenuBar parentMenu;
 +    protected CustomMenuItem selected;
 +
 +    boolean enabled = true;
 +
 +    private VLazyExecutor iconLoadedExecutioner = new VLazyExecutor(100,
 +            new ScheduledCommand() {
 +
-                         + Icon.CLASSNAME + "\" alt=\"\" />");
++        @Override
++        public void execute() {
++            iLayout(true);
++        }
++    });
 +
 +    boolean openRootOnHover;
 +
 +    boolean htmlContentAllowed;
 +
 +    public VMenuBar() {
 +        // Create an empty horizontal menubar
 +        this(false, null);
 +
 +        // Navigation is only handled by the root bar
 +        addFocusHandler(this);
 +
 +        /*
 +         * 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()) {
 +            addKeyPressHandler(this);
 +        } else {
 +            addKeyDownHandler(this);
 +        }
 +    }
 +
 +    public VMenuBar(boolean subMenu, VMenuBar parentMenu) {
 +
 +        items = new ArrayList<CustomMenuItem>();
 +        popup = null;
 +        visibleChildMenu = null;
 +
 +        containerElement = getElement();
 +
 +        if (!subMenu) {
 +            setStyleName(CLASSNAME);
 +        } else {
 +            setStyleName(CLASSNAME + "-submenu");
 +            this.parentMenu = parentMenu;
 +        }
 +        this.subMenu = subMenu;
 +
 +        sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT
 +                | Event.ONLOAD);
 +    }
 +
 +    @Override
 +    protected void onDetach() {
 +        super.onDetach();
 +        if (!subMenu) {
 +            setSelected(null);
 +            hideChildren();
 +            menuVisible = false;
 +        }
 +    }
 +
 +    void updateSize() {
 +        // Take from setWidth
 +        if (!subMenu) {
 +            // Only needed for root level menu
 +            hideChildren();
 +            setSelected(null);
 +            menuVisible = false;
 +        }
 +    }
 +
 +    /**
 +     * Build the HTML content for a menu item.
 +     * 
 +     * @param item
 +     * @return
 +     */
 +    protected String buildItemHTML(UIDL item) {
 +        // Construct html from the text and the optional icon
 +        StringBuffer itemHTML = new StringBuffer();
 +        if (item.hasAttribute("separator")) {
 +            itemHTML.append("<span>---</span>");
 +        } else {
 +            // Add submenu indicator
 +            if (item.getChildCount() > 0) {
 +                String bgStyle = "";
 +                itemHTML.append("<span class=\"" + CLASSNAME
 +                        + "-submenu-indicator\"" + bgStyle + ">&#x25BA;</span>");
 +            }
 +
 +            itemHTML.append("<span class=\"" + CLASSNAME
 +                    + "-menuitem-caption\">");
 +            if (item.hasAttribute("icon")) {
 +                itemHTML.append("<img src=\""
 +                        + Util.escapeAttribute(client.translateVaadinUri(item
 +                                .getStringAttribute("icon"))) + "\" class=\""
-                 popup.sizeOrPositionUpdated();
++                                + Icon.CLASSNAME + "\" alt=\"\" />");
 +            }
 +            String itemText = item.getStringAttribute("text");
 +            if (!htmlContentAllowed) {
 +                itemText = Util.escapeHTML(itemText);
 +            }
 +            itemHTML.append(itemText);
 +            itemHTML.append("</span>");
 +        }
 +        return itemHTML.toString();
 +    }
 +
 +    /**
 +     * This is called by the items in the menu and it communicates the
 +     * information to the server
 +     * 
 +     * @param clickedItemId
 +     *            id of the item that was clicked
 +     */
 +    public void onMenuClick(int clickedItemId) {
 +        // Updating the state to the server can not be done before
 +        // the server connection is known, i.e., before updateFromUIDL()
 +        // has been called.
 +        if (uidlId != null && client != null) {
 +            // Communicate the user interaction parameters to server. This call
 +            // will initiate an AJAX request to the server.
 +            client.updateVariable(uidlId, "clickedId", clickedItemId, true);
 +        }
 +    }
 +
 +    /** Widget methods **/
 +
 +    /**
 +     * Returns a list of items in this menu
 +     */
 +    public List<CustomMenuItem> getItems() {
 +        return items;
 +    }
 +
 +    /**
 +     * Remove all the items in this menu
 +     */
 +    public void clearItems() {
 +        Element e = getContainerElement();
 +        while (DOM.getChildCount(e) > 0) {
 +            DOM.removeChild(e, DOM.getChild(e, 0));
 +        }
 +        items.clear();
 +    }
 +
 +    /**
 +     * Returns the containing element of the menu
 +     * 
 +     * @return
 +     */
 +    @Override
 +    public Element getContainerElement() {
 +        return containerElement;
 +    }
 +
 +    /**
 +     * Add a new item to this menu
 +     * 
 +     * @param html
 +     *            items text
 +     * @param cmd
 +     *            items command
 +     * @return the item created
 +     */
 +    public CustomMenuItem addItem(String html, Command cmd) {
 +        CustomMenuItem item = GWT.create(CustomMenuItem.class);
 +        item.setHTML(html);
 +        item.setCommand(cmd);
 +
 +        addItem(item);
 +        return item;
 +    }
 +
 +    /**
 +     * Add a new item to this menu
 +     * 
 +     * @param item
 +     */
 +    public void addItem(CustomMenuItem item) {
 +        if (items.contains(item)) {
 +            return;
 +        }
 +        DOM.appendChild(getContainerElement(), item.getElement());
 +        item.setParentMenu(this);
 +        item.setSelected(false);
 +        items.add(item);
 +    }
 +
 +    public void addItem(CustomMenuItem item, int index) {
 +        if (items.contains(item)) {
 +            return;
 +        }
 +        DOM.insertChild(getContainerElement(), item.getElement(), index);
 +        item.setParentMenu(this);
 +        item.setSelected(false);
 +        items.add(index, item);
 +    }
 +
 +    /**
 +     * Remove the given item from this menu
 +     * 
 +     * @param item
 +     */
 +    public void removeItem(CustomMenuItem item) {
 +        if (items.contains(item)) {
 +            int index = items.indexOf(item);
 +
 +            DOM.removeChild(getContainerElement(),
 +                    DOM.getChild(getContainerElement(), index));
 +            items.remove(index);
 +        }
 +    }
 +
 +    /*
 +     * @see
 +     * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user
 +     * .client.Event)
 +     */
 +    @Override
 +    public void onBrowserEvent(Event e) {
 +        super.onBrowserEvent(e);
 +
 +        // Handle onload events (icon loaded, size changes)
 +        if (DOM.eventGetType(e) == Event.ONLOAD) {
 +            VMenuBar parent = getParentMenu();
 +            if (parent != null) {
 +                // The onload event for an image in a popup should be sent to
 +                // the parent, which owns the popup
 +                parent.iconLoaded();
 +            } else {
 +                // Onload events for images in the root menu are handled by the
 +                // root menu itself
 +                iconLoaded();
 +            }
 +            return;
 +        }
 +
 +        Element targetElement = DOM.eventGetTarget(e);
 +        CustomMenuItem targetItem = null;
 +        for (int i = 0; i < items.size(); i++) {
 +            CustomMenuItem item = items.get(i);
 +            if (DOM.isOrHasChild(item.getElement(), targetElement)) {
 +                targetItem = item;
 +            }
 +        }
 +
 +        if (targetItem != null) {
 +            switch (DOM.eventGetType(e)) {
 +
 +            case Event.ONCLICK:
 +                if (isEnabled() && targetItem.isEnabled()) {
 +                    itemClick(targetItem);
 +                }
 +                if (subMenu) {
 +                    // Prevent moving keyboard focus to child menus
 +                    VMenuBar parent = parentMenu;
 +                    while (parent.getParentMenu() != null) {
 +                        parent = parent.getParentMenu();
 +                    }
 +                    parent.setFocus(true);
 +                }
 +
 +                break;
 +
 +            case Event.ONMOUSEOVER:
 +                LazyCloser.cancelClosing();
 +
 +                if (isEnabled() && targetItem.isEnabled()) {
 +                    itemOver(targetItem);
 +                }
 +                break;
 +
 +            case Event.ONMOUSEOUT:
 +                itemOut(targetItem);
 +                LazyCloser.schedule();
 +                break;
 +            }
 +        } else if (subMenu && DOM.eventGetType(e) == Event.ONCLICK && subMenu) {
 +            // Prevent moving keyboard focus to child menus
 +            VMenuBar parent = parentMenu;
 +            while (parent.getParentMenu() != null) {
 +                parent = parent.getParentMenu();
 +            }
 +            parent.setFocus(true);
 +        }
 +    }
 +
 +    private boolean isEnabled() {
 +        return enabled;
 +    }
 +
 +    private void iconLoaded() {
 +        iconLoadedExecutioner.trigger();
 +    }
 +
 +    /**
 +     * When an item is clicked
 +     * 
 +     * @param item
 +     */
 +    public void itemClick(CustomMenuItem item) {
 +        if (item.getCommand() != null) {
 +            setSelected(null);
 +
 +            if (visibleChildMenu != null) {
 +                visibleChildMenu.hideChildren();
 +            }
 +
 +            hideParents(true);
 +            menuVisible = false;
 +            Scheduler.get().scheduleDeferred(item.getCommand());
 +
 +        } else {
 +            if (item.getSubMenu() != null
 +                    && item.getSubMenu() != visibleChildMenu) {
 +                setSelected(item);
 +                showChildMenu(item);
 +                menuVisible = true;
 +            } else if (!subMenu) {
 +                setSelected(null);
 +                hideChildren();
 +                menuVisible = false;
 +            }
 +        }
 +    }
 +
 +    /**
 +     * When the user hovers the mouse over the item
 +     * 
 +     * @param item
 +     */
 +    public void itemOver(CustomMenuItem item) {
 +        if ((openRootOnHover || subMenu || menuVisible) && !item.isSeparator()) {
 +            setSelected(item);
 +            if (!subMenu && openRootOnHover && !menuVisible) {
 +                menuVisible = true; // start opening menus
 +                LazyCloser.prepare(this);
 +            }
 +        }
 +
 +        if (menuVisible && visibleChildMenu != item.getSubMenu()
 +                && popup != null) {
 +            popup.hide();
 +        }
 +
 +        if (menuVisible && item.getSubMenu() != null
 +                && visibleChildMenu != item.getSubMenu()) {
 +            showChildMenu(item);
 +        }
 +    }
 +
 +    /**
 +     * When the mouse is moved away from an item
 +     * 
 +     * @param item
 +     */
 +    public void itemOut(CustomMenuItem item) {
 +        if (visibleChildMenu != item.getSubMenu()) {
 +            hideChildMenu(item);
 +            setSelected(null);
 +        } else if (visibleChildMenu == null) {
 +            setSelected(null);
 +        }
 +    }
 +
 +    /**
 +     * Used to autoclose submenus when they the menu is in a mode which opens
 +     * root menus on mouse hover.
 +     */
 +    private static class LazyCloser extends Timer {
 +        static LazyCloser INSTANCE;
 +        private VMenuBar activeRoot;
 +
 +        @Override
 +        public void run() {
 +            activeRoot.hideChildren();
 +            activeRoot.setSelected(null);
 +            activeRoot.menuVisible = false;
 +            activeRoot = null;
 +        }
 +
 +        public static void cancelClosing() {
 +            if (INSTANCE != null) {
 +                INSTANCE.cancel();
 +            }
 +        }
 +
 +        public static void prepare(VMenuBar vMenuBar) {
 +            if (INSTANCE == null) {
 +                INSTANCE = new LazyCloser();
 +            }
 +            if (INSTANCE.activeRoot == vMenuBar) {
 +                INSTANCE.cancel();
 +            } else if (INSTANCE.activeRoot != null) {
 +                INSTANCE.cancel();
 +                INSTANCE.run();
 +            }
 +            INSTANCE.activeRoot = vMenuBar;
 +        }
 +
 +        public static void schedule() {
 +            if (INSTANCE != null && INSTANCE.activeRoot != null) {
 +                INSTANCE.schedule(750);
 +            }
 +        }
 +
 +    }
 +
 +    /**
 +     * Shows the child menu of an item. The caller must ensure that the item has
 +     * a submenu.
 +     * 
 +     * @param item
 +     */
 +    public void showChildMenu(CustomMenuItem item) {
 +
 +        int left = 0;
 +        int top = 0;
 +        if (subMenu) {
 +            left = item.getParentMenu().getAbsoluteLeft()
 +                    + item.getParentMenu().getOffsetWidth();
 +            top = item.getAbsoluteTop();
 +        } else {
 +            left = item.getAbsoluteLeft();
 +            top = item.getParentMenu().getAbsoluteTop()
 +                    + item.getParentMenu().getOffsetHeight();
 +        }
 +        showChildMenuAt(item, top, left);
 +    }
 +
 +    protected void showChildMenuAt(CustomMenuItem item, int top, int left) {
 +        final int shadowSpace = 10;
 +
 +        popup = new VOverlay(true, false, true);
 +
 +        // Setting owner and handlers to support tooltips. Needed for tooltip
 +        // handling of overlay widgets (will direct queries to parent menu)
 +        if (parentMenu == null) {
 +            popup.setOwner(this);
 +        } else {
 +            VMenuBar parent = parentMenu;
 +            while (parent.getParentMenu() != null) {
 +                parent = parent.getParentMenu();
 +            }
 +            popup.setOwner(parent);
 +        }
 +        if (client != null) {
 +            client.getVTooltip().connectHandlersToWidget(popup);
 +        }
 +
 +        popup.setStyleName(CLASSNAME + "-popup");
 +        popup.setWidget(item.getSubMenu());
 +        popup.addCloseHandler(this);
 +        popup.addAutoHidePartner(item.getElement());
 +
 +        // at 0,0 because otherwise IE7 add extra scrollbars (#5547)
 +        popup.setPopupPosition(0, 0);
 +
 +        item.getSubMenu().onShow();
 +        visibleChildMenu = item.getSubMenu();
 +        item.getSubMenu().setParentMenu(this);
 +
 +        popup.show();
 +
 +        if (left + popup.getOffsetWidth() >= RootPanel.getBodyElement()
 +                .getOffsetWidth() - shadowSpace) {
 +            if (subMenu) {
 +                left = item.getParentMenu().getAbsoluteLeft()
 +                        - popup.getOffsetWidth() - shadowSpace;
 +            } else {
 +                left = RootPanel.getBodyElement().getOffsetWidth()
 +                        - popup.getOffsetWidth() - shadowSpace;
 +            }
 +            // Accommodate space for shadow
 +            if (left < shadowSpace) {
 +                left = shadowSpace;
 +            }
 +        }
 +
 +        top = adjustPopupHeight(top, shadowSpace);
 +
 +        popup.setPopupPosition(left, top);
 +
 +    }
 +
 +    private int adjustPopupHeight(int top, final int shadowSpace) {
 +        // Check that the popup will fit the screen
 +        int availableHeight = RootPanel.getBodyElement().getOffsetHeight()
 +                - top - shadowSpace;
 +        int missingHeight = popup.getOffsetHeight() - availableHeight;
 +        if (missingHeight > 0) {
 +            // First move the top of the popup to get more space
 +            // Don't move above top of screen, don't move more than needed
 +            int moveUpBy = Math.min(top - shadowSpace, missingHeight);
 +
 +            // Update state
 +            top -= moveUpBy;
 +            missingHeight -= moveUpBy;
 +            availableHeight += moveUpBy;
 +
 +            if (missingHeight > 0) {
 +                int contentWidth = visibleChildMenu.getOffsetWidth();
 +
 +                // If there's still not enough room, limit height to fit and add
 +                // a scroll bar
 +                Style style = popup.getElement().getStyle();
 +                style.setHeight(availableHeight, Unit.PX);
 +                style.setOverflowY(Overflow.SCROLL);
 +
 +                // Make room for the scroll bar by adjusting the width of the
 +                // popup
 +                style.setWidth(contentWidth + Util.getNativeScrollbarSize(),
 +                        Unit.PX);
++                popup.positionOrSizeUpdated();
 +            }
 +        }
 +        return top;
 +    }
 +
 +    /**
 +     * Hides the submenu of an item
 +     * 
 +     * @param item
 +     */
 +    public void hideChildMenu(CustomMenuItem item) {
 +        if (visibleChildMenu != null
 +                && !(visibleChildMenu == item.getSubMenu())) {
 +            popup.hide();
 +        }
 +    }
 +
 +    /**
 +     * When the menu is shown.
 +     */
 +    public void onShow() {
 +        // remove possible previous selection
 +        if (selected != null) {
 +            selected.setSelected(false);
 +            selected = null;
 +        }
 +        menuVisible = true;
 +    }
 +
 +    /**
 +     * Listener method, fired when this menu is closed
 +     */
 +    @Override
 +    public void onClose(CloseEvent<PopupPanel> event) {
 +        hideChildren();
 +        if (event.isAutoClosed()) {
 +            hideParents(true);
 +            menuVisible = false;
 +        }
 +        visibleChildMenu = null;
 +        popup = null;
 +    }
 +
 +    /**
 +     * Recursively hide all child menus
 +     */
 +    public void hideChildren() {
 +        if (visibleChildMenu != null) {
 +            visibleChildMenu.hideChildren();
 +            popup.hide();
 +        }
 +    }
 +
 +    /**
 +     * Recursively hide all parent menus
 +     */
 +    public void hideParents(boolean autoClosed) {
 +        if (visibleChildMenu != null) {
 +            popup.hide();
 +            setSelected(null);
 +            menuVisible = !autoClosed;
 +        }
 +
 +        if (getParentMenu() != null) {
 +            getParentMenu().hideParents(autoClosed);
 +        }
 +    }
 +
 +    /**
 +     * Returns the parent menu of this menu, or null if this is the top-level
 +     * menu
 +     * 
 +     * @return
 +     */
 +    public VMenuBar getParentMenu() {
 +        return parentMenu;
 +    }
 +
 +    /**
 +     * Set the parent menu of this menu
 +     * 
 +     * @param parent
 +     */
 +    public void setParentMenu(VMenuBar parent) {
 +        parentMenu = parent;
 +    }
 +
 +    /**
 +     * Returns the currently selected item of this menu, or null if nothing is
 +     * selected
 +     * 
 +     * @return
 +     */
 +    public CustomMenuItem getSelected() {
 +        return selected;
 +    }
 +
 +    /**
 +     * Set the currently selected item of this menu
 +     * 
 +     * @param item
 +     */
 +    public void setSelected(CustomMenuItem item) {
 +        // If we had something selected, unselect
 +        if (item != selected && selected != null) {
 +            selected.setSelected(false);
 +        }
 +        // If we have a valid selection, select it
 +        if (item != null) {
 +            item.setSelected(true);
 +        }
 +
 +        selected = item;
 +    }
 +
 +    /**
 +     * 
 +     * A class to hold information on menu items
 +     * 
 +     */
 +    public static class CustomMenuItem extends Widget implements HasHTML {
 +
 +        protected String html = null;
 +        protected Command command = null;
 +        protected VMenuBar subMenu = null;
 +        protected VMenuBar parentMenu = null;
 +        protected boolean enabled = true;
 +        protected boolean isSeparator = false;
 +        protected boolean checkable = false;
 +        protected boolean checked = false;
 +        protected String description = null;
 +
 +        /**
 +         * Default menu item {@link Widget} constructor for GWT.create().
 +         * 
 +         * Use {@link #setHTML(String)} and {@link #setCommand(Command)} after
 +         * constructing a menu item.
 +         */
 +        public CustomMenuItem() {
 +            this("", null);
 +        }
 +
 +        /**
 +         * Creates a menu item {@link Widget}.
 +         * 
 +         * @param html
 +         * @param cmd
 +         * @deprecated use the default constructor and {@link #setHTML(String)}
 +         *             and {@link #setCommand(Command)} instead
 +         */
 +        @Deprecated
 +        public CustomMenuItem(String html, Command cmd) {
 +            // We need spans to allow inline-block in IE
 +            setElement(DOM.createSpan());
 +
 +            setHTML(html);
 +            setCommand(cmd);
 +            setSelected(false);
 +            setStyleName(CLASSNAME + "-menuitem");
 +
 +        }
 +
 +        public void setSelected(boolean selected) {
 +            if (selected && isSelectable()) {
 +                addStyleDependentName("selected");
 +                // needed for IE6 to have a single style name to match for an
 +                // element
 +                // TODO Can be optimized now that IE6 is not supported any more
 +                if (checkable) {
 +                    if (checked) {
 +                        removeStyleDependentName("selected-unchecked");
 +                        addStyleDependentName("selected-checked");
 +                    } else {
 +                        removeStyleDependentName("selected-checked");
 +                        addStyleDependentName("selected-unchecked");
 +                    }
 +                }
 +            } else {
 +                removeStyleDependentName("selected");
 +                // needed for IE6 to have a single style name to match for an
 +                // element
 +                removeStyleDependentName("selected-checked");
 +                removeStyleDependentName("selected-unchecked");
 +            }
 +        }
 +
 +        public void setChecked(boolean checked) {
 +            if (checkable && !isSeparator) {
 +                this.checked = checked;
 +
 +                if (checked) {
 +                    addStyleDependentName("checked");
 +                    removeStyleDependentName("unchecked");
 +                } else {
 +                    addStyleDependentName("unchecked");
 +                    removeStyleDependentName("checked");
 +                }
 +            } else {
 +                this.checked = false;
 +            }
 +        }
 +
 +        public boolean isChecked() {
 +            return checked;
 +        }
 +
 +        public void setCheckable(boolean checkable) {
 +            if (checkable && !isSeparator) {
 +                this.checkable = true;
 +            } else {
 +                setChecked(false);
 +                this.checkable = false;
 +            }
 +        }
 +
 +        public boolean isCheckable() {
 +            return checkable;
 +        }
 +
 +        /*
 +         * setters and getters for the fields
 +         */
 +
 +        public void setSubMenu(VMenuBar subMenu) {
 +            this.subMenu = subMenu;
 +        }
 +
 +        public VMenuBar getSubMenu() {
 +            return subMenu;
 +        }
 +
 +        public void setParentMenu(VMenuBar parentMenu) {
 +            this.parentMenu = parentMenu;
 +        }
 +
 +        public VMenuBar getParentMenu() {
 +            return parentMenu;
 +        }
 +
 +        public void setCommand(Command command) {
 +            this.command = command;
 +        }
 +
 +        public Command getCommand() {
 +            return command;
 +        }
 +
 +        @Override
 +        public String getHTML() {
 +            return html;
 +        }
 +
 +        @Override
 +        public void setHTML(String html) {
 +            this.html = html;
 +            DOM.setInnerHTML(getElement(), html);
 +
 +            // Sink the onload event for any icons. The onload
 +            // events are handled by the parent VMenuBar.
 +            Util.sinkOnloadForImages(getElement());
 +        }
 +
 +        @Override
 +        public String getText() {
 +            return html;
 +        }
 +
 +        @Override
 +        public void setText(String text) {
 +            setHTML(Util.escapeHTML(text));
 +        }
 +
 +        public void setEnabled(boolean enabled) {
 +            this.enabled = enabled;
 +            if (enabled) {
 +                removeStyleDependentName("disabled");
 +            } else {
 +                addStyleDependentName("disabled");
 +            }
 +        }
 +
 +        public boolean isEnabled() {
 +            return enabled;
 +        }
 +
 +        private void setSeparator(boolean separator) {
 +            isSeparator = separator;
 +            if (separator) {
 +                setStyleName(CLASSNAME + "-separator");
 +            } else {
 +                setStyleName(CLASSNAME + "-menuitem");
 +                setEnabled(enabled);
 +            }
 +        }
 +
 +        public boolean isSeparator() {
 +            return isSeparator;
 +        }
 +
 +        public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
 +            setSeparator(uidl.hasAttribute("separator"));
 +            setEnabled(!uidl
 +                    .hasAttribute(MenuBarConstants.ATTRIBUTE_ITEM_DISABLED));
 +
 +            if (!isSeparator()
 +                    && uidl.hasAttribute(MenuBarConstants.ATTRIBUTE_CHECKED)) {
 +                // if the selected attribute is present (either true or false),
 +                // the item is selectable
 +                setCheckable(true);
 +                setChecked(uidl
 +                        .getBooleanAttribute(MenuBarConstants.ATTRIBUTE_CHECKED));
 +            } else {
 +                setCheckable(false);
 +            }
 +
 +            if (uidl.hasAttribute(MenuBarConstants.ATTRIBUTE_ITEM_STYLE)) {
 +                String itemStyle = uidl
 +                        .getStringAttribute(MenuBarConstants.ATTRIBUTE_ITEM_STYLE);
 +                addStyleDependentName(itemStyle);
 +            }
 +
 +            if (uidl.hasAttribute(MenuBarConstants.ATTRIBUTE_ITEM_DESCRIPTION)) {
 +                description = uidl
 +                        .getStringAttribute(MenuBarConstants.ATTRIBUTE_ITEM_DESCRIPTION);
 +            }
 +        }
 +
 +        public TooltipInfo getTooltip() {
 +            if (description == null) {
 +                return null;
 +            }
 +
 +            return new TooltipInfo(description);
 +        }
 +
 +        /**
 +         * Checks if the item can be selected.
 +         * 
 +         * @return true if it is possible to select this item, false otherwise
 +         */
 +        public boolean isSelectable() {
 +            return !isSeparator() && isEnabled();
 +        }
 +
 +    }
 +
 +    /**
 +     * @author Jouni Koivuviita / Vaadin Ltd.
 +     */
 +    public void iLayout() {
 +        iLayout(false);
 +        updateSize();
 +    }
 +
 +    public void iLayout(boolean iconLoadEvent) {
 +        // Only collapse if there is more than one item in the root menu and the
 +        // menu has an explicit size
 +        if ((getItems().size() > 1 || (collapsedRootItems != null && collapsedRootItems
 +                .getItems().size() > 0))
 +                && getElement().getStyle().getProperty("width") != null
 +                && moreItem != null) {
 +
 +            // Measure the width of the "more" item
 +            final boolean morePresent = getItems().contains(moreItem);
 +            addItem(moreItem);
 +            final int moreItemWidth = moreItem.getOffsetWidth();
 +            if (!morePresent) {
 +                removeItem(moreItem);
 +            }
 +
 +            int availableWidth = LayoutManager.get(client).getInnerWidth(
 +                    getElement());
 +
 +            // Used width includes the "more" item if present
 +            int usedWidth = getConsumedWidth();
 +            int diff = availableWidth - usedWidth;
 +            removeItem(moreItem);
 +
 +            if (diff < 0) {
 +                // Too many items: collapse last items from root menu
 +                int widthNeeded = usedWidth - availableWidth;
 +                if (!morePresent) {
 +                    widthNeeded += moreItemWidth;
 +                }
 +                int widthReduced = 0;
 +
 +                while (widthReduced < widthNeeded && getItems().size() > 0) {
 +                    // Move last root menu item to collapsed menu
 +                    CustomMenuItem collapse = getItems().get(
 +                            getItems().size() - 1);
 +                    widthReduced += collapse.getOffsetWidth();
 +                    removeItem(collapse);
 +                    collapsedRootItems.addItem(collapse, 0);
 +                }
 +            } else if (collapsedRootItems.getItems().size() > 0) {
 +                // Space available for items: expand first items from collapsed
 +                // menu
 +                int widthAvailable = diff + moreItemWidth;
 +                int widthGrowth = 0;
 +
 +                while (widthAvailable > widthGrowth
 +                        && collapsedRootItems.getItems().size() > 0) {
 +                    // Move first item from collapsed menu to the root menu
 +                    CustomMenuItem expand = collapsedRootItems.getItems()
 +                            .get(0);
 +                    collapsedRootItems.removeItem(expand);
 +                    addItem(expand);
 +                    widthGrowth += expand.getOffsetWidth();
 +                    if (collapsedRootItems.getItems().size() > 0) {
 +                        widthAvailable -= moreItemWidth;
 +                    }
 +                    if (widthGrowth > widthAvailable) {
 +                        removeItem(expand);
 +                        collapsedRootItems.addItem(expand, 0);
 +                    } else {
 +                        widthAvailable = diff + moreItemWidth;
 +                    }
 +                }
 +            }
 +            if (collapsedRootItems.getItems().size() > 0) {
 +                addItem(moreItem);
 +            }
 +        }
 +
 +        // If a popup is open we might need to adjust the shadow as well if an
 +        // icon shown in that popup was loaded
 +        if (popup != null) {
 +            // Forces a recalculation of the shadow size
 +            popup.show();
 +        }
 +        if (iconLoadEvent) {
 +            // Size have changed if the width is undefined
 +            Util.notifyParentOfSizeChange(this, false);
 +        }
 +    }
 +
 +    private int getConsumedWidth() {
 +        int w = 0;
 +        for (CustomMenuItem item : getItems()) {
 +            if (!collapsedRootItems.getItems().contains(item)) {
 +                w += item.getOffsetWidth();
 +            }
 +        }
 +        return w;
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
 +     * .gwt.event.dom.client.KeyPressEvent)
 +     */
 +    @Override
 +    public void onKeyPress(KeyPressEvent event) {
 +        if (handleNavigation(event.getNativeEvent().getKeyCode(),
 +                event.isControlKeyDown() || event.isMetaKeyDown(),
 +                event.isShiftKeyDown())) {
 +            event.preventDefault();
 +        }
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
 +     * .event.dom.client.KeyDownEvent)
 +     */
 +    @Override
 +    public void onKeyDown(KeyDownEvent event) {
 +        if (handleNavigation(event.getNativeEvent().getKeyCode(),
 +                event.isControlKeyDown() || event.isMetaKeyDown(),
 +                event.isShiftKeyDown())) {
 +            event.preventDefault();
 +        }
 +    }
 +
 +    /**
 +     * Get the key that moves the selection 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 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 moves the selection left. 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 moves the selection right. 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 a menu item. By default it is the Enter key but
 +     * by overriding this you can change the key to whatever you want.
 +     * 
 +     * @return
 +     */
 +    protected int getNavigationSelectKey() {
 +        return KeyCodes.KEY_ENTER;
 +    }
 +
 +    /**
 +     * Get the key that closes the menu. By default it is the escape key but by
 +     * overriding this yoy can change the key to whatever you want.
 +     * 
 +     * @return
 +     */
 +    protected int getCloseMenuKey() {
 +        return KeyCodes.KEY_ESCAPE;
 +    }
 +
 +    /**
 +     * Handles the keyboard events handled by the MenuBar
 +     * 
 +     * @param event
 +     *            The keyboard event received
 +     * @return true iff the navigation event was handled
 +     */
 +    public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
 +
 +        // If tab or shift+tab close menus
 +        if (keycode == KeyCodes.KEY_TAB) {
 +            setSelected(null);
 +            hideChildren();
 +            menuVisible = false;
 +            return false;
 +        }
 +
 +        if (ctrl || shift || !isEnabled()) {
 +            // Do not handle tab key, nor ctrl keys
 +            return false;
 +        }
 +
 +        if (keycode == getNavigationLeftKey()) {
 +            if (getSelected() == null) {
 +                // If nothing is selected then select the last item
 +                setSelected(items.get(items.size() - 1));
 +                if (!getSelected().isSelectable()) {
 +                    handleNavigation(keycode, ctrl, shift);
 +                }
 +            } else if (visibleChildMenu == null && getParentMenu() == null) {
 +                // If this is the root menu then move to the left
 +                int idx = items.indexOf(getSelected());
 +                if (idx > 0) {
 +                    setSelected(items.get(idx - 1));
 +                } else {
 +                    setSelected(items.get(items.size() - 1));
 +                }
 +
 +                if (!getSelected().isSelectable()) {
 +                    handleNavigation(keycode, ctrl, shift);
 +                }
 +            } else if (visibleChildMenu != null) {
 +                // Redirect all navigation to the submenu
 +                visibleChildMenu.handleNavigation(keycode, ctrl, shift);
 +
 +            } else if (getParentMenu().getParentMenu() == null) {
 +                // Inside a sub menu, whose parent is a root menu item
 +                VMenuBar root = getParentMenu();
 +
 +                root.getSelected().getSubMenu().setSelected(null);
 +                root.hideChildren();
 +
 +                // Get the root menus items and select the previous one
 +                int idx = root.getItems().indexOf(root.getSelected());
 +                idx = idx > 0 ? idx : root.getItems().size();
 +                CustomMenuItem selected = root.getItems().get(--idx);
 +
 +                while (selected.isSeparator() || !selected.isEnabled()) {
 +                    idx = idx > 0 ? idx : root.getItems().size();
 +                    selected = root.getItems().get(--idx);
 +                }
 +
 +                root.setSelected(selected);
 +                openMenuAndFocusFirstIfPossible(selected);
 +            } else {
 +                getParentMenu().getSelected().getSubMenu().setSelected(null);
 +                getParentMenu().hideChildren();
 +            }
 +
 +            return true;
 +
 +        } else if (keycode == getNavigationRightKey()) {
 +
 +            if (getSelected() == null) {
 +                // If nothing is selected then select the first item
 +                setSelected(items.get(0));
 +                if (!getSelected().isSelectable()) {
 +                    handleNavigation(keycode, ctrl, shift);
 +                }
 +            } else if (visibleChildMenu == null && getParentMenu() == null) {
 +                // If this is the root menu then move to the right
 +                int idx = items.indexOf(getSelected());
 +
 +                if (idx < items.size() - 1) {
 +                    setSelected(items.get(idx + 1));
 +                } else {
 +                    setSelected(items.get(0));
 +                }
 +
 +                if (!getSelected().isSelectable()) {
 +                    handleNavigation(keycode, ctrl, shift);
 +                }
 +            } else if (visibleChildMenu == null
 +                    && getSelected().getSubMenu() != null) {
 +                // If the item has a submenu then show it and move the selection
 +                // there
 +                showChildMenu(getSelected());
 +                menuVisible = true;
 +                visibleChildMenu.handleNavigation(keycode, ctrl, shift);
 +            } else if (visibleChildMenu == null) {
 +
 +                // Get the root menu
 +                VMenuBar root = getParentMenu();
 +                while (root.getParentMenu() != null) {
 +                    root = root.getParentMenu();
 +                }
 +
 +                // Hide the submenu
 +                root.hideChildren();
 +
 +                // Get the root menus items and select the next one
 +                int idx = root.getItems().indexOf(root.getSelected());
 +                idx = idx < root.getItems().size() - 1 ? idx : -1;
 +                CustomMenuItem selected = root.getItems().get(++idx);
 +
 +                while (selected.isSeparator() || !selected.isEnabled()) {
 +                    idx = idx < root.getItems().size() - 1 ? idx : -1;
 +                    selected = root.getItems().get(++idx);
 +                }
 +
 +                root.setSelected(selected);
 +                openMenuAndFocusFirstIfPossible(selected);
 +            } else if (visibleChildMenu != null) {
 +                // Redirect all navigation to the submenu
 +                visibleChildMenu.handleNavigation(keycode, ctrl, shift);
 +            }
 +
 +            return true;
 +
 +        } else if (keycode == getNavigationUpKey()) {
 +
 +            if (getSelected() == null) {
 +                // If nothing is selected then select the last item
 +                setSelected(items.get(items.size() - 1));
 +                if (!getSelected().isSelectable()) {
 +                    handleNavigation(keycode, ctrl, shift);
 +                }
 +            } else if (visibleChildMenu != null) {
 +                // Redirect all navigation to the submenu
 +                visibleChildMenu.handleNavigation(keycode, ctrl, shift);
 +            } else {
 +                // Select the previous item if possible or loop to the last item
 +                int idx = items.indexOf(getSelected());
 +                if (idx > 0) {
 +                    setSelected(items.get(idx - 1));
 +                } else {
 +                    setSelected(items.get(items.size() - 1));
 +                }
 +
 +                if (!getSelected().isSelectable()) {
 +                    handleNavigation(keycode, ctrl, shift);
 +                }
 +            }
 +
 +            return true;
 +
 +        } else if (keycode == getNavigationDownKey()) {
 +
 +            if (getSelected() == null) {
 +                // If nothing is selected then select the first item
 +                selectFirstItem();
 +            } else if (visibleChildMenu == null && getParentMenu() == null) {
 +                // If this is the root menu the show the child menu with arrow
 +                // down, if there is a child menu
 +                openMenuAndFocusFirstIfPossible(getSelected());
 +            } else if (visibleChildMenu != null) {
 +                // Redirect all navigation to the submenu
 +                visibleChildMenu.handleNavigation(keycode, ctrl, shift);
 +            } else {
 +                // Select the next item if possible or loop to the first item
 +                int idx = items.indexOf(getSelected());
 +                if (idx < items.size() - 1) {
 +                    setSelected(items.get(idx + 1));
 +                } else {
 +                    setSelected(items.get(0));
 +                }
 +
 +                if (!getSelected().isSelectable()) {
 +                    handleNavigation(keycode, ctrl, shift);
 +                }
 +            }
 +            return true;
 +
 +        } else if (keycode == getCloseMenuKey()) {
 +            setSelected(null);
 +            hideChildren();
 +            menuVisible = false;
 +
 +        } else if (keycode == getNavigationSelectKey()) {
 +            if (getSelected() == null) {
 +                // If nothing is selected then select the first item
 +                selectFirstItem();
 +            } else if (visibleChildMenu != null) {
 +                // Redirect all navigation to the submenu
 +                visibleChildMenu.handleNavigation(keycode, ctrl, shift);
 +                menuVisible = false;
 +            } else if (visibleChildMenu == null
 +                    && getSelected().getSubMenu() != null) {
 +                // If the item has a sub menu then show it and move the
 +                // selection there
 +                openMenuAndFocusFirstIfPossible(getSelected());
 +            } else {
 +                Command command = getSelected().getCommand();
 +                if (command != null) {
 +                    command.execute();
 +                }
 +
 +                setSelected(null);
 +                hideParents(true);
 +            }
 +        }
 +
 +        return false;
 +    }
 +
 +    private void selectFirstItem() {
 +        for (int i = 0; i < items.size(); i++) {
 +            CustomMenuItem item = items.get(i);
 +            if (item.isSelectable()) {
 +                setSelected(item);
 +                break;
 +            }
 +        }
 +    }
 +
 +    private void openMenuAndFocusFirstIfPossible(CustomMenuItem menuItem) {
 +        VMenuBar subMenu = menuItem.getSubMenu();
 +        if (subMenu == null) {
 +            // No child menu? Nothing to do
 +            return;
 +        }
 +
 +        VMenuBar parentMenu = menuItem.getParentMenu();
 +        parentMenu.showChildMenu(menuItem);
 +
 +        menuVisible = true;
 +        // Select the first item in the newly open submenu
 +        subMenu.selectFirstItem();
 +
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
 +     * .dom.client.FocusEvent)
 +     */
 +    @Override
 +    public void onFocus(FocusEvent event) {
 +
 +    }
 +
 +    private final String SUBPART_PREFIX = "item";
 +
 +    @Override
 +    public Element getSubPartElement(String subPart) {
 +        int index = Integer
 +                .parseInt(subPart.substring(SUBPART_PREFIX.length()));
 +        CustomMenuItem item = getItems().get(index);
 +
 +        return item.getElement();
 +    }
 +
 +    @Override
 +    public String getSubPartName(Element subElement) {
 +        if (!getElement().isOrHasChild(subElement)) {
 +            return null;
 +        }
 +
 +        Element menuItemRoot = subElement;
 +        while (menuItemRoot != null && menuItemRoot.getParentElement() != null
 +                && menuItemRoot.getParentElement() != getElement()) {
 +            menuItemRoot = menuItemRoot.getParentElement().cast();
 +        }
 +        // "menuItemRoot" is now the root of the menu item
 +
 +        final int itemCount = getItems().size();
 +        for (int i = 0; i < itemCount; i++) {
 +            if (getItems().get(i).getElement() == menuItemRoot) {
 +                String name = SUBPART_PREFIX + i;
 +                return name;
 +            }
 +        }
 +        return null;
 +    }
 +
 +    /**
 +     * Get menu item with given DOM element
 +     * 
 +     * @param element
 +     *            Element used in search
 +     * @return Menu item or null if not found
 +     */
 +    public CustomMenuItem getMenuItemWithElement(Element element) {
 +        for (int i = 0; i < items.size(); i++) {
 +            CustomMenuItem item = items.get(i);
 +            if (DOM.isOrHasChild(item.getElement(), element)) {
 +                return item;
 +            }
 +
 +            if (item.getSubMenu() != null) {
 +                item = item.getSubMenu().getMenuItemWithElement(element);
 +                if (item != null) {
 +                    return item;
 +                }
 +            }
 +        }
 +
 +        return null;
 +    }
 +}
index 451e6badbe33a32776f10cdf6c0ad93eeddb0c6f,0000000000000000000000000000000000000000..6e253c913794430e970d4253766c1179441ed22b
mode 100644,000000..100644
--- /dev/null
@@@ -1,468 -1,0 +1,468 @@@
-         sizeOrPositionUpdated();
 +/* 
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.notification;
 +
 +import java.util.ArrayList;
 +import java.util.Date;
 +import java.util.EventObject;
 +import java.util.Iterator;
 +
 +import com.google.gwt.core.client.GWT;
 +import com.google.gwt.event.dom.client.KeyCodes;
 +import com.google.gwt.user.client.DOM;
 +import com.google.gwt.user.client.Element;
 +import com.google.gwt.user.client.Event;
 +import com.google.gwt.user.client.Timer;
 +import com.google.gwt.user.client.ui.HTML;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.shared.ui.root.RootConstants;
 +import com.vaadin.terminal.gwt.client.ApplicationConnection;
 +import com.vaadin.terminal.gwt.client.BrowserInfo;
 +import com.vaadin.terminal.gwt.client.UIDL;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.ui.VOverlay;
 +
 +public class VNotification extends VOverlay {
 +
 +    public static final int CENTERED = 1;
 +    public static final int CENTERED_TOP = 2;
 +    public static final int CENTERED_BOTTOM = 3;
 +    public static final int TOP_LEFT = 4;
 +    public static final int TOP_RIGHT = 5;
 +    public static final int BOTTOM_LEFT = 6;
 +    public static final int BOTTOM_RIGHT = 7;
 +
 +    public static final int DELAY_FOREVER = -1;
 +    public static final int DELAY_NONE = 0;
 +
 +    private static final String STYLENAME = "v-Notification";
 +    private static final int mouseMoveThreshold = 7;
 +    private static final int Z_INDEX_BASE = 20000;
 +    public static final String STYLE_SYSTEM = "system";
 +    private static final int FADE_ANIMATION_INTERVAL = 50; // == 20 fps
 +
 +    private static final ArrayList<VNotification> notifications = new ArrayList<VNotification>();
 +
 +    private int startOpacity = 90;
 +    private int fadeMsec = 400;
 +    private int delayMsec = 1000;
 +
 +    private Timer fader;
 +    private Timer delay;
 +
 +    private int x = -1;
 +    private int y = -1;
 +
 +    private String temporaryStyle;
 +
 +    private ArrayList<EventListener> listeners;
 +    private static final int TOUCH_DEVICE_IDLE_DELAY = 1000;
 +
 +    /**
 +     * Default constructor. You should use GWT.create instead.
 +     */
 +    public VNotification() {
 +        setStyleName(STYLENAME);
 +        sinkEvents(Event.ONCLICK);
 +        DOM.setStyleAttribute(getElement(), "zIndex", "" + Z_INDEX_BASE);
 +    }
 +
 +    /**
 +     * @deprecated Use static {@link #createNotification(int)} instead to enable
 +     *             GWT deferred binding.
 +     * 
 +     * @param delayMsec
 +     */
 +    @Deprecated
 +    public VNotification(int delayMsec) {
 +        this();
 +        this.delayMsec = delayMsec;
 +        if (BrowserInfo.get().isTouchDevice()) {
 +            new Timer() {
 +                @Override
 +                public void run() {
 +                    if (isAttached()) {
 +                        fade();
 +                    }
 +                }
 +            }.schedule(delayMsec + TOUCH_DEVICE_IDLE_DELAY);
 +        }
 +    }
 +
 +    /**
 +     * @deprecated Use static {@link #createNotification(int, int, int)} instead
 +     *             to enable GWT deferred binding.
 +     * 
 +     * @param delayMsec
 +     * @param fadeMsec
 +     * @param startOpacity
 +     */
 +    @Deprecated
 +    public VNotification(int delayMsec, int fadeMsec, int startOpacity) {
 +        this(delayMsec);
 +        this.fadeMsec = fadeMsec;
 +        this.startOpacity = startOpacity;
 +    }
 +
 +    public void startDelay() {
 +        DOM.removeEventPreview(this);
 +        if (delayMsec > 0) {
 +            if (delay == null) {
 +                delay = new Timer() {
 +                    @Override
 +                    public void run() {
 +                        fade();
 +                    }
 +                };
 +                delay.schedule(delayMsec);
 +            }
 +        } else if (delayMsec == 0) {
 +            fade();
 +        }
 +    }
 +
 +    @Override
 +    public void show() {
 +        show(CENTERED);
 +    }
 +
 +    public void show(String style) {
 +        show(CENTERED, style);
 +    }
 +
 +    public void show(int position) {
 +        show(position, null);
 +    }
 +
 +    public void show(Widget widget, int position, String style) {
 +        setWidget(widget);
 +        show(position, style);
 +    }
 +
 +    public void show(String html, int position, String style) {
 +        setWidget(new HTML(html));
 +        show(position, style);
 +    }
 +
 +    public void show(int position, String style) {
 +        setOpacity(getElement(), startOpacity);
 +        if (style != null) {
 +            temporaryStyle = style;
 +            addStyleName(style);
 +            addStyleDependentName(style);
 +        }
 +        super.show();
 +        notifications.add(this);
 +        setPosition(position);
++        positionOrSizeUpdated();
 +        /**
 +         * Android 4 fails to render notifications correctly without a little
 +         * nudge (#8551)
 +         */
 +        if (BrowserInfo.get().isAndroid()) {
 +            Util.setStyleTemporarily(getElement(), "display", "none");
 +        }
 +    }
 +
 +    @Override
 +    public void hide() {
 +        DOM.removeEventPreview(this);
 +        cancelDelay();
 +        cancelFade();
 +        if (temporaryStyle != null) {
 +            removeStyleName(temporaryStyle);
 +            removeStyleDependentName(temporaryStyle);
 +            temporaryStyle = null;
 +        }
 +        super.hide();
 +        notifications.remove(this);
 +        fireEvent(new HideEvent(this));
 +    }
 +
 +    public void fade() {
 +        DOM.removeEventPreview(this);
 +        cancelDelay();
 +        if (fader == null) {
 +            fader = new Timer() {
 +                private final long start = new Date().getTime();
 +
 +                @Override
 +                public void run() {
 +                    /*
 +                     * To make animation smooth, don't count that event happens
 +                     * on time. Reduce opacity according to the actual time
 +                     * spent instead of fixed decrement.
 +                     */
 +                    long now = new Date().getTime();
 +                    long timeEplaced = now - start;
 +                    float remainingFraction = 1 - timeEplaced
 +                            / (float) fadeMsec;
 +                    int opacity = (int) (startOpacity * remainingFraction);
 +                    if (opacity <= 0) {
 +                        cancel();
 +                        hide();
 +                        if (BrowserInfo.get().isOpera()) {
 +                            // tray notification on opera needs to explicitly
 +                            // define
 +                            // size, reset it
 +                            DOM.setStyleAttribute(getElement(), "width", "");
 +                            DOM.setStyleAttribute(getElement(), "height", "");
 +                        }
 +                    } else {
 +                        setOpacity(getElement(), opacity);
 +                    }
 +                }
 +            };
 +            fader.scheduleRepeating(FADE_ANIMATION_INTERVAL);
 +        }
 +    }
 +
 +    public void setPosition(int position) {
 +        final Element el = getElement();
 +        DOM.setStyleAttribute(el, "top", "");
 +        DOM.setStyleAttribute(el, "left", "");
 +        DOM.setStyleAttribute(el, "bottom", "");
 +        DOM.setStyleAttribute(el, "right", "");
 +        switch (position) {
 +        case TOP_LEFT:
 +            DOM.setStyleAttribute(el, "top", "0px");
 +            DOM.setStyleAttribute(el, "left", "0px");
 +            break;
 +        case TOP_RIGHT:
 +            DOM.setStyleAttribute(el, "top", "0px");
 +            DOM.setStyleAttribute(el, "right", "0px");
 +            break;
 +        case BOTTOM_RIGHT:
 +            DOM.setStyleAttribute(el, "position", "absolute");
 +            if (BrowserInfo.get().isOpera()) {
 +                // tray notification on opera needs explicitly defined size
 +                DOM.setStyleAttribute(el, "width", getOffsetWidth() + "px");
 +                DOM.setStyleAttribute(el, "height", getOffsetHeight() + "px");
 +            }
 +            DOM.setStyleAttribute(el, "bottom", "0px");
 +            DOM.setStyleAttribute(el, "right", "0px");
 +            break;
 +        case BOTTOM_LEFT:
 +            DOM.setStyleAttribute(el, "bottom", "0px");
 +            DOM.setStyleAttribute(el, "left", "0px");
 +            break;
 +        case CENTERED_TOP:
 +            center();
 +            DOM.setStyleAttribute(el, "top", "0px");
 +            break;
 +        case CENTERED_BOTTOM:
 +            center();
 +            DOM.setStyleAttribute(el, "top", "");
 +            DOM.setStyleAttribute(el, "bottom", "0px");
 +            break;
 +        default:
 +        case CENTERED:
 +            center();
 +            break;
 +        }
 +    }
 +
 +    private void cancelFade() {
 +        if (fader != null) {
 +            fader.cancel();
 +            fader = null;
 +        }
 +    }
 +
 +    private void cancelDelay() {
 +        if (delay != null) {
 +            delay.cancel();
 +            delay = null;
 +        }
 +    }
 +
 +    private void setOpacity(Element el, int opacity) {
 +        DOM.setStyleAttribute(el, "opacity", "" + (opacity / 100.0));
 +        if (BrowserInfo.get().isIE()) {
 +            DOM.setStyleAttribute(el, "filter", "Alpha(opacity=" + opacity
 +                    + ")");
 +        }
 +    }
 +
 +    @Override
 +    public void onBrowserEvent(Event event) {
 +        DOM.removeEventPreview(this);
 +        if (fader == null) {
 +            fade();
 +        }
 +    }
 +
 +    @Override
 +    public boolean onEventPreview(Event event) {
 +        int type = DOM.eventGetType(event);
 +        // "modal"
 +        if (delayMsec == -1 || temporaryStyle == STYLE_SYSTEM) {
 +            if (type == Event.ONCLICK) {
 +                if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
 +                    fade();
 +                    return false;
 +                }
 +            } else if (type == Event.ONKEYDOWN
 +                    && event.getKeyCode() == KeyCodes.KEY_ESCAPE) {
 +                fade();
 +                return false;
 +            }
 +            if (temporaryStyle == STYLE_SYSTEM) {
 +                return true;
 +            } else {
 +                return false;
 +            }
 +        }
 +        // default
 +        switch (type) {
 +        case Event.ONMOUSEMOVE:
 +
 +            if (x < 0) {
 +                x = DOM.eventGetClientX(event);
 +                y = DOM.eventGetClientY(event);
 +            } else if (Math.abs(DOM.eventGetClientX(event) - x) > mouseMoveThreshold
 +                    || Math.abs(DOM.eventGetClientY(event) - y) > mouseMoveThreshold) {
 +                startDelay();
 +            }
 +            break;
 +        case Event.ONMOUSEDOWN:
 +        case Event.ONMOUSEWHEEL:
 +        case Event.ONSCROLL:
 +            startDelay();
 +            break;
 +        case Event.ONKEYDOWN:
 +            if (event.getRepeat()) {
 +                return true;
 +            }
 +            startDelay();
 +            break;
 +        default:
 +            break;
 +        }
 +        return true;
 +    }
 +
 +    public void addEventListener(EventListener listener) {
 +        if (listeners == null) {
 +            listeners = new ArrayList<EventListener>();
 +        }
 +        listeners.add(listener);
 +    }
 +
 +    public void removeEventListener(EventListener listener) {
 +        if (listeners == null) {
 +            return;
 +        }
 +        listeners.remove(listener);
 +    }
 +
 +    private void fireEvent(HideEvent event) {
 +        if (listeners != null) {
 +            for (Iterator<EventListener> it = listeners.iterator(); it
 +                    .hasNext();) {
 +                EventListener l = it.next();
 +                l.notificationHidden(event);
 +            }
 +        }
 +    }
 +
 +    public static void showNotification(ApplicationConnection client,
 +            final UIDL notification) {
 +        boolean onlyPlainText = notification
 +                .hasAttribute(RootConstants.NOTIFICATION_HTML_CONTENT_NOT_ALLOWED);
 +        String html = "";
 +        if (notification
 +                .hasAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_ICON)) {
 +            final String parsedUri = client
 +                    .translateVaadinUri(notification
 +                            .getStringAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_ICON));
 +            html += "<img src=\"" + Util.escapeAttribute(parsedUri) + "\" />";
 +        }
 +        if (notification
 +                .hasAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_CAPTION)) {
 +            String caption = notification
 +                    .getStringAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_CAPTION);
 +            if (onlyPlainText) {
 +                caption = Util.escapeHTML(caption);
 +                caption = caption.replaceAll("\\n", "<br />");
 +            }
 +            html += "<h1>" + caption + "</h1>";
 +        }
 +        if (notification
 +                .hasAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_MESSAGE)) {
 +            String message = notification
 +                    .getStringAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_MESSAGE);
 +            if (onlyPlainText) {
 +                message = Util.escapeHTML(message);
 +                message = message.replaceAll("\\n", "<br />");
 +            }
 +            html += "<p>" + message + "</p>";
 +        }
 +
 +        final String style = notification
 +                .hasAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_STYLE) ? notification
 +                .getStringAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_STYLE)
 +                : null;
 +        final int position = notification
 +                .getIntAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_POSITION);
 +        final int delay = notification
 +                .getIntAttribute(RootConstants.ATTRIBUTE_NOTIFICATION_DELAY);
 +        createNotification(delay).show(html, position, style);
 +    }
 +
 +    public static VNotification createNotification(int delayMsec) {
 +        final VNotification notification = GWT.create(VNotification.class);
 +        notification.delayMsec = delayMsec;
 +        if (BrowserInfo.get().isTouchDevice()) {
 +            new Timer() {
 +                @Override
 +                public void run() {
 +                    if (notification.isAttached()) {
 +                        notification.fade();
 +                    }
 +                }
 +            }.schedule(notification.delayMsec + TOUCH_DEVICE_IDLE_DELAY);
 +        }
 +        return notification;
 +    }
 +
 +    public class HideEvent extends EventObject {
 +
 +        public HideEvent(Object source) {
 +            super(source);
 +        }
 +    }
 +
 +    public interface EventListener extends java.util.EventListener {
 +        public void notificationHidden(HideEvent event);
 +    }
 +
 +    /**
 +     * Moves currently visible notifications to the top of the event preview
 +     * stack. Can be called when opening other overlays such as subwindows to
 +     * ensure the notifications receive the events they need and don't linger
 +     * indefinitely. See #7136.
 +     * 
 +     * TODO Should this be a generic Overlay feature instead?
 +     */
 +    public static void bringNotificationsToFront() {
 +        for (VNotification notification : notifications) {
 +            DOM.removeEventPreview(notification);
 +            DOM.addEventPreview(notification);
 +        }
 +    }
 +}
index aa7da488d827c27156be7e701c60d51796e97d73,0000000000000000000000000000000000000000..345eebc8aa8e7d6d2977662bdd401f84b70f0814
mode 100644,000000..100644
--- /dev/null
@@@ -1,6917 -1,0 +1,6942 @@@
-             adopt(row);
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +
 +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.Map;
 +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.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.shared.ComponentState;
 +import com.vaadin.shared.MouseEventDetails;
 +import com.vaadin.shared.ui.dd.VerticalDropLocation;
 +import com.vaadin.shared.ui.table.TableConstants;
 +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.ConnectorMap;
 +import com.vaadin.terminal.gwt.client.Focusable;
 +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.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";
 +
 +    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
 +     */
 +    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;
 +
 +    private static final boolean hasNativeTouchScrolling = BrowserInfo.get()
 +            .isTouchDevice()
 +            && !BrowserInfo.get().requiresTouchScrollDelegate();
 +
 +    private Set<String> noncollapsibleColumns;
 +
 +    /**
 +     * The last known row height used to preserve the height of a table with
 +     * custom row heights and a fixed page length after removing the last row
 +     * from the table.
 +     * 
 +     * A new VScrollTableBody instance is created every time the number of rows
 +     * changes causing {@link VScrollTableBody#rowHeight} to be discarded and
 +     * the height recalculated by {@link VScrollTableBody#getRowHeight(boolean)}
 +     * to avoid some rounding problems, e.g. round(2 * 19.8) / 2 = 20 but
 +     * round(3 * 19.8) / 3 = 19.66.
 +     */
 +    private double lastKnownRowHeight = Double.NaN;
 +
 +    /**
 +     * 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() {
 +
 +        @Override
 +        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() {
 +
 +        @Override
 +        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() {
 +
 +        @Override
 +        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.addStyleName(CLASSNAME + "-body-wrapper");
 +        scrollBodyPanel.addFocusHandler(this);
 +        scrollBodyPanel.addBlurHandler(this);
 +
 +        scrollBodyPanel.addScrollHandler(this);
 +        scrollBodyPanel.addStyleName(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.sinkEvents(Event.ONCONTEXTMENU);
 +        scrollBodyPanel.addDomHandler(new ContextMenuHandler() {
 +
 +            @Override
 +            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>() {
 +
 +            @Override
 +            public void onClose(CloseEvent<PopupPanel> event) {
 +                contextMenu = null;
 +            }
 +        });
 +    }
 +
 +    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"));
 +        if (uidl.hasVariable("noncollapsiblecolumns")) {
 +            noncollapsibleColumns = uidl
 +                    .getStringArrayVariableAsSet("noncollapsiblecolumns");
 +        }
 +    }
 +
 +    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,
 +                TableConstants.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) {
 +            // Deferred due to some Firefox oddities
 +            Scheduler.get().scheduleDeferred(new Command() {
 +
 +                @Override
 +                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() {
 +
 +            @Override
 +            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() {
 +
 +                        @Override
 +                        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,
 +                                    TableConstants.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,
 +                    TableConstants.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() {
 +
 +                                    @Override
 +                                    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() {
 +
 +                    @Override
 +                    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);
 +        }
 +
 +        @Override
 +        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 boolean noncollapsible = false;
 +            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() {
 +                if (noncollapsible) {
 +                    return;
 +                }
 +                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;
 +            }
 +
 +            public void setNoncollapsible(boolean b) {
 +                noncollapsible = b;
 +            }
 +
 +            /**
 +             * Override default method to distinguish on/off columns
 +             */
 +
 +            @Override
 +            public String getHTML() {
 +                final StringBuffer buf = new StringBuffer();
 +                buf.append("<span class=\"");
 +                if (collapsed) {
 +                    buf.append("v-off");
 +                } else {
 +                    buf.append("v-on");
 +                }
 +                if (noncollapsible) {
 +                    buf.append(" v-disabled");
 +                }
 +                buf.append("\">");
 +
 +                buf.append(super.getHTML());
 +                buf.append("</span>");
 +
 +                return buf.toString();
 +            }
 +
 +        }
 +
 +        /*
 +         * Returns columns as Action array for column select popup
 +         */
 +
 +        @Override
 +        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);
 +                }
 +                if (noncollapsibleColumns.contains(cid)) {
 +                    a.setNoncollapsible(true);
 +                }
 +                actions[i] = a;
 +            }
 +            return actions;
 +        }
 +
 +        @Override
 +        public ApplicationConnection getClient() {
 +            return client;
 +        }
 +
 +        @Override
 +        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) {
 +            if (footerText == null || footerText.equals("")) {
 +                footerText = "&nbsp;";
 +            }
 +
 +            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.
 +                 */
 +                final int borderWidths = 1;
 +
 +                // Set the container width (check for negative value)
 +                captionContainer.getStyle().setPropertyPx("width",
 +                        Math.max(w - borderWidths, 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) {
 +                    int tdWidth = width + scrollBody.getCellExtraWidth()
 +                            - borderWidths;
 +                    setWidth(Math.max(tdWidth, 0) + "px");
 +                } else {
 +                    Scheduler.get().scheduleDeferred(new Command() {
 +
 +                        @Override
 +                        public void execute() {
 +                            int tdWidth = width
 +                                    + scrollBody.getCellExtraWidth()
 +                                    - borderWidths;
 +                            setWidth(Math.max(tdWidth, 0) + "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,
 +                                TableConstants.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,
 +                    TableConstants.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()
 +         */
 +
 +        @Override
 +        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().requiresTouchScrollDelegate()) {
 +                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());
-             availW -= scrollBody.getCellExtraWidth() * visibleCellCount;
++            // Add to renderedRows before adopt so iterator() will return also
++            // this row if called in an attach handler (#9264)
 +            renderedRows.add(row);
++            adopt(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);
 +        }
 +
 +        @Override
 +        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);
 +            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 {
 +                    // Special cases if we can't just measure the current rows
 +                    if (!Double.isNaN(lastKnownRowHeight)) {
 +                        // Use previous value if available
 +                        if (BrowserInfo.get().isIE()) {
 +                            /*
 +                             * IE needs to reflow the table element at this
 +                             * point to work correctly (e.g.
 +                             * com.vaadin.tests.components.table.
 +                             * ContainerSizeChange) - the other code paths
 +                             * already trigger reflows, but here it must be done
 +                             * explicitly.
 +                             */
 +                            getTableHeight();
 +                        }
 +                        rowHeight = lastKnownRowHeight;
 +                    } 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;
 +                    }
 +                }
 +                lastKnownRowHeight = rowHeight;
 +                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 {
 +
 +            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 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 Timer dragTouchTimeout;
 +            private int touchStartY;
 +            private int touchStartX;
 +            private TooltipInfo tooltipInfo = null;
 +            private Map<TableCellElement, TooltipInfo> cellToolTips = new HashMap<TableCellElement, TooltipInfo>();
 +            private boolean isDragging = false;
 +
 +            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 = new TooltipInfo(rowDescription);
 +                } else {
 +                    tooltipInfo = 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();
 +                }
 +            }
 +
 +            public TooltipInfo getTooltipInfo() {
 +                return tooltipInfo;
 +            }
 +
 +            /**
 +             * 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);
 +                    cellToolTips.put(td, info);
 +                } else {
 +                    cellToolTips.remove(td);
 +                }
 +
 +                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);
 +            }
 +
 +            @Override
 +            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,
 +                        TableConstants.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;
 +            }
 +
 +            public TooltipInfo getTooltip(
 +                    com.google.gwt.dom.client.Element target) {
 +
 +                TooltipInfo info = null;
 +
 +                if (target.hasTagName("TD")) {
 +
 +                    TableCellElement td = (TableCellElement) target.cast();
 +                    info = cellToolTips.get(td);
 +                }
 +
 +                if (info == null) {
 +                    info = tooltipInfo;
 +                }
 +
 +                return info;
 +            }
 +
 +            /**
 +             * Special handler for touch devices that support native scrolling
 +             * 
 +             * @return Whether the event was handled by this method.
 +             */
 +            private boolean handleTouchEvent(final Event event) {
 +
 +                boolean touchEventHandled = false;
 +
 +                if (enabled && hasNativeTouchScrolling) {
 +                    final Element targetTdOrTr = getEventTargetTdOrTr(event);
 +                    final int type = event.getTypeInt();
 +
 +                    switch (type) {
 +                    case Event.ONTOUCHSTART:
 +                        touchEventHandled = true;
 +                        touchStart = event;
 +                        isDragging = false;
 +                        Touch touch = event.getChangedTouches().get(0);
 +                        // save position to fields, touches in events are same
 +                        // instance during the operation.
 +                        touchStartX = touch.getClientX();
 +                        touchStartY = touch.getClientY();
 +
 +                        if (dragmode != 0) {
 +                            if (dragTouchTimeout == null) {
 +                                dragTouchTimeout = new Timer() {
 +
 +                                    @Override
 +                                    public void run() {
 +                                        if (touchStart != null) {
 +                                            // Start a drag if a finger is held
 +                                            // in place long enough, then moved
 +                                            isDragging = true;
 +                                        }
 +                                    }
 +                                };
 +                            }
 +                            dragTouchTimeout.schedule(TOUCHSCROLL_TIMEOUT);
 +                        }
 +
 +                        if (actionKeys != null) {
 +                            if (contextTouchTimeout == null) {
 +                                contextTouchTimeout = new Timer() {
 +
 +                                    @Override
 +                                    public void run() {
 +                                        if (touchStart != null) {
 +                                            // Open the context menu if finger
 +                                            // is held in place long enough.
 +                                            showContextMenu(touchStart);
 +                                            event.preventDefault();
 +                                            touchStart = null;
 +                                        }
 +                                    }
 +                                };
 +                            }
 +                            contextTouchTimeout
 +                                    .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
 +                        }
 +                        break;
 +                    case Event.ONTOUCHMOVE:
 +                        touchEventHandled = true;
 +                        if (isSignificantMove(event)) {
 +                            if (contextTouchTimeout != null) {
 +                                // Moved finger before the context menu timer
 +                                // expired, so let the browser handle this as a
 +                                // scroll.
 +                                contextTouchTimeout.cancel();
 +                                contextTouchTimeout = null;
 +                            }
 +                            if (!isDragging && dragTouchTimeout != null) {
 +                                // Moved finger before the drag timer expired,
 +                                // so let the browser handle this as a scroll.
 +                                dragTouchTimeout.cancel();
 +                                dragTouchTimeout = null;
 +                            }
 +
 +                            if (dragmode != 0 && touchStart != null
 +                                    && isDragging) {
 +                                event.preventDefault();
 +                                event.stopPropagation();
 +                                startRowDrag(touchStart, type, targetTdOrTr);
 +                            }
 +                            touchStart = null;
 +                        }
 +                        break;
 +                    case Event.ONTOUCHEND:
 +                    case Event.ONTOUCHCANCEL:
 +                        touchEventHandled = true;
 +                        if (contextTouchTimeout != null) {
 +                            contextTouchTimeout.cancel();
 +                        }
 +                        if (dragTouchTimeout != null) {
 +                            dragTouchTimeout.cancel();
 +                        }
 +                        if (touchStart != null) {
 +                            event.preventDefault();
 +                            event.stopPropagation();
 +                            if (!BrowserInfo.get().isAndroid()) {
 +                                Util.simulateClickFromTouchEvent(touchStart,
 +                                        this);
 +                            }
 +                            touchStart = null;
 +                        }
 +                        isDragging = false;
 +                        break;
 +                    }
 +                }
 +                return touchEventHandled;
 +            }
 +
 +            /*
 +             * React on click that occur on content cells only
 +             */
 +
 +            @Override
 +            public void onBrowserEvent(final Event event) {
 +
 +                final boolean touchEventHandled = handleTouchEvent(event);
 +
 +                if (enabled && !touchEventHandled) {
 +                    final int type = event.getTypeInt();
 +                    final Element targetTdOrTr = getEventTargetTdOrTr(event);
 +                    if (type == Event.ONCONTEXTMENU) {
 +                        showContextMenu(event);
 +                        if (enabled
 +                                && (actionKeys != null || client
 +                                        .hasEventListeners(
 +                                                VScrollTable.this,
 +                                                TableConstants.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;
 +
 +                    switch (type) {
 +                    case Event.ONDBLCLICK:
 +                        if (targetCellOrRowFound) {
 +                            handleClickEvent(event, targetTdOrTr, true);
 +                        }
 +                        break;
 +                    case Event.ONMOUSEUP:
 +                        if (targetCellOrRowFound) {
 +                            /*
 +                             * 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:
 +                        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) {
 +                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 ()
 +             */
 +
 +            @Override
 +            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;
 +            }
 +
 +            @Override
 +            public ApplicationConnection getClient() {
 +                return client;
 +            }
 +
 +            @Override
 +            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() {
 +
 +                    @Override
 +                    public void execute() {
 +                        if (showRowHeaders) {
 +                            setCellWidth(0, tHead.getHeaderCell(0).getWidth());
 +                            calcAndSetSpanWidthOnCell(1);
 +                        } else {
 +                            calcAndSetSpanWidthOnCell(0);
 +                        }
 +                    }
 +                });
 +            }
 +
 +            @Override
 +            protected boolean isRenderHtmlInCells() {
 +                return htmlContentAllowed;
 +            }
 +
 +            @Override
 +            protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
 +                    int visibleColumnIndex) {
 +                htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
 +                spanColumns = uidl.getBooleanAttribute("gen_span");
 +
 +                final Iterator<?> cells = uidl.getChildIterator();
 +                if (spanColumns) {
 +                    int colCount = uidl.getChildCount();
 +                    if (cells.hasNext()) {
 +                        final Object cell = cells.next();
 +                        if (cell instanceof String) {
 +                            addSpannedCell(uidl, cell.toString(), aligns[0],
 +                                    "", htmlContentAllowed, false, null,
 +                                    colCount);
 +                        } else {
 +                            addSpannedCell(uidl, (Widget) cell, aligns[0], "",
 +                                    false, colCount);
 +                        }
 +                    }
 +                } else {
 +                    super.addCellsFromUIDL(uidl, aligns, col,
 +                            visibleColumnIndex);
 +                }
 +            }
 +
 +            private void addSpannedCell(UIDL rowUidl, Widget w, char align,
 +                    String style, boolean sorted, int colCount) {
 +                TableCellElement td = DOM.createTD().cast();
 +                td.setColSpan(colCount);
 +                initCellWithWidget(w, align, style, sorted, td);
 +            }
 +
 +            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 -= Util.getNativeScrollbarSize();
++            int totalExtraWidth = scrollBody.getCellExtraWidth()
++                    * visibleCellCount;
 +            if (willHaveScrollbars()) {
++                totalExtraWidth += Util.getNativeScrollbarSize();
 +            }
++            availW -= totalExtraWidth;
++            int forceScrollBodyWidth = -1;
 +
 +            int extraSpace = availW - usedMinimumWidth;
 +            if (extraSpace < 0) {
++                if (getTotalRows() == 0) {
++                    /*
++                     * Too wide header combined with no rows in the table.
++                     * 
++                     * No horizontal scrollbars would be displayed because
++                     * there's no rows that grows too wide causing the
++                     * scrollBody container div to overflow. Must explicitely
++                     * force a width to a scrollbar. (see #9187)
++                     */
++                    forceScrollBodyWidth = usedMinimumWidth + totalExtraWidth;
++                }
 +                extraSpace = 0;
 +            }
 +
++            if (forceScrollBodyWidth > 0) {
++                scrollBody.container.getStyle().setWidth(forceScrollBodyWidth,
++                        Unit.PX);
++            } else {
++                // Clear width that might have been set to force horizontal
++                // scrolling if there are no rows
++                scrollBody.container.getStyle().clearWidth();
++            }
++
 +            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() {
 +
 +                @Override
 +                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();
 +
 +        if (initializedAndAttached) {
 +            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.
 +            // We must run the fix as a deferred command to prevent it from
 +            // overwriting the scroll position with an outdated value, see
 +            // #7607.
 +            Scheduler.get().scheduleDeferred(new Command() {
 +
 +                @Override
 +                public void execute() {
 +                    Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
 +                }
 +            });
 +        }
 +
 +        triggerLazyColumnAdjustment(false);
 +
 +        /*
 +         * 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() {
 +
 +                        @Override
 +                        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
 +     */
 +
 +    @Override
 +    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() {
 +
 +                @Override
 +                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());
 +    }
 +
 +    @Override
 +    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;
 +        }
 +
 +        //
 +        // 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() {
 +
 +                    @Override
 +                    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);
 +        }
 +
 +        @Override
 +        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)
 +     */
 +
 +    @Override
 +    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)
 +     */
 +
 +    @Override
 +    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()
 +     */
 +
 +    @Override
 +    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() {
 +
 +            @Override
 +            public void execute() {
 +                if (currentlyFocusedRow != null) {
 +                    setRowFocus(currentlyFocusedRow);
 +                } else {
 +                    VConsole.log("no row?");
 +                    focusRowFromBody();
 +                }
 +                scrollBody.ensureFocus();
 +            }
 +        });
 +    }
 +
 +    @Override
 +    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;
 +    }
 +
 +    @Override
 +    public ApplicationConnection getClient() {
 +        return client;
 +    }
 +
 +    @Override
 +    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 e061cda1fa572a75d606af9e8ba9a65245a26de1,0000000000000000000000000000000000000000..e1df1ba0db33ca9ae7b63818e40ce687fabba307
mode 100644,000000..100644
--- /dev/null
@@@ -1,117 -1,0 +1,170 @@@
-     @Override
-     protected void setMaxLength(int newMaxLength) {
-         super.setMaxLength(newMaxLength);
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.textarea;
 +
 +import com.google.gwt.core.client.Scheduler;
 +import com.google.gwt.dom.client.Style.Overflow;
 +import com.google.gwt.dom.client.TextAreaElement;
++import com.google.gwt.event.dom.client.ChangeEvent;
++import com.google.gwt.event.dom.client.ChangeHandler;
++import com.google.gwt.event.dom.client.KeyUpEvent;
++import com.google.gwt.event.dom.client.KeyUpHandler;
 +import com.google.gwt.user.client.Command;
 +import com.google.gwt.user.client.DOM;
 +import com.google.gwt.user.client.Event;
 +import com.vaadin.terminal.gwt.client.BrowserInfo;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
 +
 +/**
 + * This class represents a multiline textfield (textarea).
 + * 
 + * TODO consider replacing this with a RichTextArea based implementation. IE
 + * does not support CSS height for textareas in Strict mode :-(
 + * 
 + * @author Vaadin Ltd.
 + * 
 + */
 +public class VTextArea extends VTextField {
 +    public static final String CLASSNAME = "v-textarea";
 +    private boolean wordwrap = true;
++    private MaxLengthHandler maxLengthHandler = new MaxLengthHandler();
++    private boolean browserSupportsMaxLengthAttribute = browserSupportsMaxLengthAttribute();
 +
 +    public VTextArea() {
 +        super(DOM.createTextArea());
 +        setStyleName(CLASSNAME);
++        if (!browserSupportsMaxLengthAttribute) {
++            addKeyUpHandler(maxLengthHandler);
++            addChangeHandler(maxLengthHandler);
++            sinkEvents(Event.ONPASTE);
++        }
 +    }
 +
 +    public TextAreaElement getTextAreaElement() {
 +        return super.getElement().cast();
 +    }
 +
 +    public void setRows(int rows) {
 +        getTextAreaElement().setRows(rows);
 +    }
 +
-         boolean hasMaxLength = (newMaxLength >= 0);
++    private class MaxLengthHandler implements KeyUpHandler, ChangeHandler {
 +
-         if (hasMaxLength) {
-             sinkEvents(Event.ONKEYUP);
-         } else {
-             unsinkEvents(Event.ONKEYUP);
++        @Override
++        public void onKeyUp(KeyUpEvent event) {
++            enforceMaxLength();
++        }
 +
-     @Override
-     public void onBrowserEvent(Event event) {
-         if (getMaxLength() >= 0 && event.getTypeInt() == Event.ONKEYUP) {
++        public void onPaste(Event event) {
++            enforceMaxLength();
 +        }
++
++        @Override
++        public void onChange(ChangeEvent event) {
++            // Opera does not support paste events so this enforces max length
++            // for Opera.
++            enforceMaxLength();
++        }
++
 +    }
 +
++    protected void enforceMaxLength() {
++        if (getMaxLength() >= 0) {
 +            Scheduler.get().scheduleDeferred(new Command() {
 +                @Override
 +                public void execute() {
 +                    if (getText().length() > getMaxLength()) {
 +                        setText(getText().substring(0, getMaxLength()));
 +                    }
 +                }
 +            });
 +        }
++    }
++
++    protected boolean browserSupportsMaxLengthAttribute() {
++        BrowserInfo info = BrowserInfo.get();
++        if (info.isFirefox() && info.isBrowserVersionNewerOrEqual(4, 0)) {
++            return true;
++        }
++        if (info.isSafari() && info.isBrowserVersionNewerOrEqual(5, 0)) {
++            return true;
++        }
++        if (info.isIE() && info.isBrowserVersionNewerOrEqual(10, 0)) {
++            return true;
++        }
++        if (info.isAndroid() && info.isBrowserVersionNewerOrEqual(2, 3)) {
++            return true;
++        }
++        return false;
++    }
++
++    @Override
++    protected void updateMaxLength(int maxLength) {
++        if (browserSupportsMaxLengthAttribute) {
++            super.updateMaxLength(maxLength);
++        } else {
++            // Events handled by MaxLengthHandler. This call enforces max length
++            // when the max length value has changed
++            enforceMaxLength();
++        }
++    }
++
++    @Override
++    public void onBrowserEvent(Event event) {
 +        super.onBrowserEvent(event);
++        if (event.getTypeInt() == Event.ONPASTE) {
++            maxLengthHandler.onPaste(event);
++        }
 +    }
 +
++
 +    @Override
 +    public int getCursorPos() {
 +        // This is needed so that TextBoxImplIE6 is used to return the correct
 +        // position for old Internet Explorer versions where it has to be
 +        // detected in a different way.
 +        return getImpl().getTextAreaCursorPos(getElement());
 +    }
 +
 +    @Override
 +    protected void setMaxLengthToElement(int newMaxLength) {
 +        // There is no maxlength property for textarea. The maximum length is
 +        // enforced by the KEYUP handler
 +
 +    }
 +
 +    public void setWordwrap(boolean wordwrap) {
 +        if (wordwrap == this.wordwrap) {
 +            return; // No change
 +        }
 +
 +        if (wordwrap) {
 +            getElement().removeAttribute("wrap");
 +            getElement().getStyle().clearOverflow();
 +        } else {
 +            getElement().setAttribute("wrap", "off");
 +            getElement().getStyle().setOverflow(Overflow.AUTO);
 +        }
 +        if (BrowserInfo.get().isOpera()) {
 +            // Opera fails to dynamically update the wrap attribute so we detach
 +            // and reattach the whole TextArea.
 +            Util.detachAttach(getElement());
 +        }
 +        this.wordwrap = wordwrap;
 +    }
 +}
index b00210cdd25c61d003bccae90f2b83abb1564bf6,0000000000000000000000000000000000000000..8f07c67c96cebcd3e3ee509498b1863bc3dcb740
mode 100644,000000..100644
--- /dev/null
@@@ -1,421 -1,0 +1,441 @@@
-         FocusHandler, BlurHandler, KeyDownHandler {
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.textfield;
 +
 +import com.google.gwt.event.dom.client.BlurEvent;
 +import com.google.gwt.event.dom.client.BlurHandler;
 +import com.google.gwt.event.dom.client.ChangeEvent;
 +import com.google.gwt.event.dom.client.ChangeHandler;
 +import com.google.gwt.event.dom.client.FocusEvent;
 +import com.google.gwt.event.dom.client.FocusHandler;
 +import com.google.gwt.event.dom.client.KeyCodes;
 +import com.google.gwt.event.dom.client.KeyDownEvent;
 +import com.google.gwt.event.dom.client.KeyDownHandler;
 +import com.google.gwt.user.client.DOM;
 +import com.google.gwt.user.client.Element;
 +import com.google.gwt.user.client.Event;
 +import com.google.gwt.user.client.Timer;
 +import com.google.gwt.user.client.ui.TextBoxBase;
 +import com.vaadin.shared.EventId;
 +import com.vaadin.shared.ui.textfield.TextFieldConstants;
 +import com.vaadin.terminal.gwt.client.ApplicationConnection;
 +import com.vaadin.terminal.gwt.client.BrowserInfo;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.ui.Field;
 +
 +/**
 + * This class represents a basic text input field with one row.
 + * 
 + * @author Vaadin Ltd.
 + * 
 + */
 +public class VTextField extends TextBoxBase implements Field, ChangeHandler,
-                         .getTypeInt()) {
++FocusHandler, BlurHandler, KeyDownHandler {
 +
 +    /**
 +     * The input node CSS classname.
 +     */
 +    public static final String CLASSNAME = "v-textfield";
 +    /**
 +     * This CSS classname is added to the input node on hover.
 +     */
 +    public static final String CLASSNAME_FOCUS = "focus";
 +
 +    protected String paintableId;
 +
 +    protected ApplicationConnection client;
 +
 +    protected String valueBeforeEdit = null;
 +
 +    /**
 +     * Set to false if a text change event has been sent since the last value
 +     * change event. This means that {@link #valueBeforeEdit} should not be
 +     * trusted when determining whether a text change even should be sent.
 +     */
 +    private boolean valueBeforeEditIsSynced = true;
 +
 +    private boolean immediate = false;
 +    private int maxLength = -1;
 +
 +    private static final String CLASSNAME_PROMPT = "prompt";
 +    private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT";
 +
 +    private String inputPrompt = null;
 +    private boolean prompting = false;
 +    private int lastCursorPos = -1;
 +
 +    public VTextField() {
 +        this(DOM.createInputText());
 +    }
 +
 +    protected VTextField(Element node) {
 +        super(node);
 +        setStyleName(CLASSNAME);
 +        addChangeHandler(this);
 +        if (BrowserInfo.get().isIE()) {
 +            // IE does not send change events when pressing enter in a text
 +            // input so we handle it using a key listener instead
 +            addKeyDownHandler(this);
 +        }
 +        addFocusHandler(this);
 +        addBlurHandler(this);
 +    }
 +
 +    /*
 +     * TODO When GWT adds ONCUT, add it there and remove workaround. See
 +     * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030
 +     * 
 +     * Also note that the cut/paste are not totally crossbrowsers compatible.
 +     * E.g. in Opera mac works via context menu, but on via File->Paste/Cut.
 +     * Opera might need the polling method for 100% working textchanceevents.
 +     * Eager polling for a change is bit dum and heavy operation, so I guess we
 +     * should first try to survive without.
 +     */
 +    protected static final int TEXTCHANGE_EVENTS = Event.ONPASTE
 +            | Event.KEYEVENTS | Event.ONMOUSEUP;
 +
 +    @Override
 +    public void onBrowserEvent(Event event) {
 +        super.onBrowserEvent(event);
 +
 +        if (listenTextChangeEvents
 +                && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event
-         if (newMaxLength >= 0) {
++                .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,
 +                        TextFieldConstants.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) {
-         } else {
++        if (newMaxLength >= 0 && newMaxLength != maxLength) {
 +            maxLength = newMaxLength;
-         setMaxLengthToElement(newMaxLength);
++            updateMaxLength(maxLength);
++        } else if (maxLength != -1) {
 +            maxLength = -1;
++            updateMaxLength(maxLength);
++        }
++
++    }
++
++    /**
++     * This method is reponsible for updating the DOM or otherwise ensuring that
++     * the given max length is enforced. Called when the max length for the
++     * field has changed.
++     * 
++     * @param maxLength
++     *            The new max length
++     */
++    protected void updateMaxLength(int maxLength) {
++        if (maxLength >= 0) {
++            getElement().setPropertyInt("maxLength", maxLength);
++        } else {
++            getElement().removeAttribute("maxLength");
++
 +        }
++        setMaxLengthToElement(maxLength);
 +    }
 +
 +    protected void setMaxLengthToElement(int newMaxLength) {
 +        if (newMaxLength >= 0) {
 +            getElement().setPropertyInt("maxLength", newMaxLength);
 +        } else {
 +            getElement().removeAttribute("maxLength");
 +        }
 +    }
 +
 +    public int getMaxLength() {
 +        return maxLength;
 +    }
 +
 +    @Override
 +    public void onChange(ChangeEvent event) {
 +        valueChange(false);
 +    }
 +
 +    /**
 +     * Called when the field value might have changed and/or the field was
 +     * blurred. These are combined so the blur event is sent in the same batch
 +     * as a possible value change event (these are often connected).
 +     * 
 +     * @param blurred
 +     *            true if the field was blurred
 +     */
 +    public void valueChange(boolean blurred) {
 +        if (client != null && paintableId != null) {
 +            boolean sendBlurEvent = false;
 +            boolean sendValueChange = false;
 +
 +            if (blurred && client.hasEventListeners(this, EventId.BLUR)) {
 +                sendBlurEvent = true;
 +                client.updateVariable(paintableId, EventId.BLUR, "", false);
 +            }
 +
 +            String newText = getText();
 +            if (!prompting && newText != null
 +                    && !newText.equals(valueBeforeEdit)) {
 +                sendValueChange = immediate;
 +                client.updateVariable(paintableId, "text", newText, false);
 +                valueBeforeEdit = newText;
 +                valueBeforeEditIsSynced = true;
 +            }
 +
 +            /*
 +             * also send cursor position, no public api yet but for easier
 +             * extension
 +             */
 +            updateCursorPosition();
 +
 +            if (sendBlurEvent || sendValueChange) {
 +                /*
 +                 * Avoid sending text change event as we will simulate it on the
 +                 * server side before value change events.
 +                 */
 +                textChangeEventTrigger.cancel();
 +                scheduled = false;
 +                client.sendPendingVariableChanges();
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Updates the cursor position variable if it has changed since the last
 +     * update.
 +     * 
 +     * @return true iff the value was updated
 +     */
 +    protected boolean updateCursorPosition() {
 +        if (Util.isAttachedAndDisplayed(this)) {
 +            int cursorPos = getCursorPos();
 +            if (lastCursorPos != cursorPos) {
 +                client.updateVariable(paintableId,
 +                        TextFieldConstants.VAR_CURSOR, cursorPos, false);
 +                lastCursorPos = cursorPos;
 +                return true;
 +            }
 +        }
 +        return false;
 +    }
 +
 +    private static VTextField focusedTextField;
 +
 +    public static void flushChangesFromFocusedTextField() {
 +        if (focusedTextField != null) {
 +            focusedTextField.onChange(null);
 +        }
 +    }
 +
 +    @Override
 +    public void onFocus(FocusEvent event) {
 +        addStyleDependentName(CLASSNAME_FOCUS);
 +        if (prompting) {
 +            setText("");
 +            removeStyleDependentName(CLASSNAME_PROMPT);
 +            setPrompting(false);
 +        }
 +        focusedTextField = this;
 +        if (client.hasEventListeners(this, EventId.FOCUS)) {
 +            client.updateVariable(paintableId, EventId.FOCUS, "", true);
 +        }
 +    }
 +
 +    @Override
 +    public void onBlur(BlurEvent event) {
 +        // this is called twice on Chrome when e.g. changing tab while prompting
 +        // field focused - do not change settings on the second time
 +        if (focusedTextField != this) {
 +            return;
 +        }
 +        removeStyleDependentName(CLASSNAME_FOCUS);
 +        focusedTextField = null;
 +        String text = getText();
 +        setPrompting(inputPrompt != null && (text == null || "".equals(text)));
 +        if (prompting) {
 +            setText(isReadOnly() ? "" : inputPrompt);
 +            addStyleDependentName(CLASSNAME_PROMPT);
 +        }
 +
 +        valueChange(true);
 +    }
 +
 +    private void setPrompting(boolean prompting) {
 +        this.prompting = prompting;
 +    }
 +
 +    public void setColumns(int columns) {
 +        if (columns <= 0) {
 +            return;
 +        }
 +
 +        setWidth(columns + "em");
 +    }
 +
 +    @Override
 +    public void onKeyDown(KeyDownEvent event) {
 +        if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
 +            valueChange(false);
 +        }
 +    }
 +
 +    public void setImmediate(boolean immediate) {
 +        this.immediate = immediate;
 +    }
 +
 +    public void setInputPrompt(String inputPrompt) {
 +        this.inputPrompt = inputPrompt;
 +    }
 +
 +}
index 9fbaa1d8bf54c409b97b57e6dfd3efece93a2010,0000000000000000000000000000000000000000..40c5e4b8afff3daa2ff059e596de97b1909fba9a
mode 100644,000000..100644
--- /dev/null
@@@ -1,2139 -1,0 +1,2137 @@@
-         FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler,
-         SubPartAware, ActionOwner {
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.tree;
 +
 +import java.util.ArrayList;
 +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.NativeEvent;
 +import com.google.gwt.dom.client.Node;
 +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.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.Window;
 +import com.google.gwt.user.client.ui.FlowPanel;
 +import com.google.gwt.user.client.ui.SimplePanel;
 +import com.google.gwt.user.client.ui.UIObject;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.shared.MouseEventDetails;
 +import com.vaadin.shared.ui.dd.VerticalDropLocation;
 +import com.vaadin.shared.ui.tree.TreeConstants;
 +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.ConnectorMap;
 +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
 +import com.vaadin.terminal.gwt.client.UIDL;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.ui.Action;
 +import com.vaadin.terminal.gwt.client.ui.ActionOwner;
 +import com.vaadin.terminal.gwt.client.ui.FocusElementPanel;
 +import com.vaadin.terminal.gwt.client.ui.Icon;
 +import com.vaadin.terminal.gwt.client.ui.SubPartAware;
 +import com.vaadin.terminal.gwt.client.ui.TreeAction;
 +import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
 +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.VDropHandler;
 +import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
 +import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
 +
 +/**
 + * 
 + */
 +public class VTree extends FocusElementPanel implements VHasDropHandler,
-                 @Override
-                 public void execute() {
-                     Util.notifyParentOfSizeChange(VTree.this, true);
-                 }
++FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler,
++SubPartAware, ActionOwner {
 +
 +    public static final String CLASSNAME = "v-tree";
 +
 +    /**
 +     * Click selects the current node, ctrl/shift toggles multi selection
 +     */
 +    public static final int MULTISELECT_MODE_DEFAULT = 0;
 +
 +    /**
 +     * Click/touch on node toggles its selected status
 +     */
 +    public static final int MULTISELECT_MODE_SIMPLE = 1;
 +
 +    private static final int CHARCODE_SPACE = 32;
 +
 +    final FlowPanel body = new FlowPanel();
 +
 +    Set<String> selectedIds = new HashSet<String>();
 +    ApplicationConnection client;
 +    String paintableId;
 +    boolean selectable;
 +    boolean isMultiselect;
 +    private String currentMouseOverKey;
 +    TreeNode lastSelection;
 +    TreeNode focusedNode;
 +    int multiSelectMode = MULTISELECT_MODE_DEFAULT;
 +
 +    private final HashMap<String, TreeNode> keyToNode = new HashMap<String, TreeNode>();
 +
 +    /**
 +     * This map contains captions and icon urls for actions like: * "33_c" ->
 +     * "Edit" * "33_i" -> "http://dom.com/edit.png"
 +     */
 +    private final HashMap<String, String> actionMap = new HashMap<String, String>();
 +
 +    boolean immediate;
 +
 +    boolean isNullSelectionAllowed = true;
 +
 +    boolean disabled = false;
 +
 +    boolean readonly;
 +
 +    boolean rendering;
 +
 +    private VAbstractDropHandler dropHandler;
 +
 +    int dragMode;
 +
 +    private boolean selectionHasChanged = false;
 +
 +    String[] bodyActionKeys;
 +
 +    public VLazyExecutor iconLoaded = new VLazyExecutor(50,
 +            new ScheduledCommand() {
 +
-             });
++        @Override
++        public void execute() {
++            Util.notifyParentOfSizeChange(VTree.this, true);
++        }
 +
-             ScheduledCommand command = new ScheduledCommand() {
++    });
 +
 +    public VTree() {
 +        super();
 +        setStyleName(CLASSNAME);
 +        add(body);
 +
 +        addFocusHandler(this);
 +        addBlurHandler(this);
 +
 +        /*
 +         * Listen to context menu events on the empty space in the tree
 +         */
 +        sinkEvents(Event.ONCONTEXTMENU);
 +        addDomHandler(new ContextMenuHandler() {
 +            @Override
 +            public void onContextMenu(ContextMenuEvent event) {
 +                handleBodyContextMenu(event);
 +            }
 +        }, ContextMenuEvent.getType());
 +
 +        /*
 +         * 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() || BrowserInfo.get().isOpera()) {
 +            addKeyPressHandler(this);
 +        } else {
 +            addKeyDownHandler(this);
 +        }
 +
 +        /*
 +         * We need to use the sinkEvents method to catch the keyUp events so we
 +         * can cache a single shift. KeyUpHandler cannot do this. At the same
 +         * time we catch the mouse down and up events so we can apply the text
 +         * selection patch in IE
 +         */
 +        sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONKEYUP);
 +
 +        /*
 +         * Re-set the tab index to make sure that the FocusElementPanel's
 +         * (super) focus element gets the tab index and not the element
 +         * containing the tree.
 +         */
 +        setTabIndex(0);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user
 +     * .client.Event)
 +     */
 +    @Override
 +    public void onBrowserEvent(Event event) {
 +        super.onBrowserEvent(event);
 +        if (event.getTypeInt() == Event.ONMOUSEDOWN) {
 +            // Prevent default text selection in IE
 +            if (BrowserInfo.get().isIE()) {
 +                ((Element) event.getEventTarget().cast()).setPropertyJSO(
 +                        "onselectstart", applyDisableTextSelectionIEHack());
 +            }
 +        } else if (event.getTypeInt() == Event.ONMOUSEUP) {
 +            // Remove IE text selection hack
 +            if (BrowserInfo.get().isIE()) {
 +                ((Element) event.getEventTarget().cast()).setPropertyJSO(
 +                        "onselectstart", null);
 +            }
 +        } else if (event.getTypeInt() == Event.ONKEYUP) {
 +            if (selectionHasChanged) {
 +                if (event.getKeyCode() == getNavigationDownKey()
 +                        && !event.getShiftKey()) {
 +                    sendSelectionToServer();
 +                    event.preventDefault();
 +                } else if (event.getKeyCode() == getNavigationUpKey()
 +                        && !event.getShiftKey()) {
 +                    sendSelectionToServer();
 +                    event.preventDefault();
 +                } else if (event.getKeyCode() == KeyCodes.KEY_SHIFT) {
 +                    sendSelectionToServer();
 +                    event.preventDefault();
 +                } else if (event.getKeyCode() == getNavigationSelectKey()) {
 +                    sendSelectionToServer();
 +                    event.preventDefault();
 +                }
 +            }
 +        }
 +    }
 +
 +    public String getActionCaption(String actionKey) {
 +        return actionMap.get(actionKey + "_c");
 +    }
 +
 +    public String getActionIcon(String actionKey) {
 +        return actionMap.get(actionKey + "_i");
 +    }
 +
 +    /**
 +     * Returns the first root node of the tree or null if there are no root
 +     * nodes.
 +     * 
 +     * @return The first root {@link TreeNode}
 +     */
 +    protected TreeNode getFirstRootNode() {
 +        if (body.getWidgetCount() == 0) {
 +            return null;
 +        }
 +        return (TreeNode) body.getWidget(0);
 +    }
 +
 +    /**
 +     * Returns the last root node of the tree or null if there are no root
 +     * nodes.
 +     * 
 +     * @return The last root {@link TreeNode}
 +     */
 +    protected TreeNode getLastRootNode() {
 +        if (body.getWidgetCount() == 0) {
 +            return null;
 +        }
 +        return (TreeNode) body.getWidget(body.getWidgetCount() - 1);
 +    }
 +
 +    /**
 +     * Returns a list of all root nodes in the Tree in the order they appear in
 +     * the tree.
 +     * 
 +     * @return A list of all root {@link TreeNode}s.
 +     */
 +    protected List<TreeNode> getRootNodes() {
 +        ArrayList<TreeNode> rootNodes = new ArrayList<TreeNode>();
 +        for (int i = 0; i < body.getWidgetCount(); i++) {
 +            rootNodes.add((TreeNode) body.getWidget(i));
 +        }
 +        return rootNodes;
 +    }
 +
 +    private void updateTreeRelatedDragData(VDragEvent drag) {
 +
 +        currentMouseOverKey = findCurrentMouseOverKey(drag.getElementOver());
 +
 +        drag.getDropDetails().put("itemIdOver", currentMouseOverKey);
 +        if (currentMouseOverKey != null) {
 +            TreeNode treeNode = getNodeByKey(currentMouseOverKey);
 +            VerticalDropLocation detail = treeNode.getDropDetail(drag
 +                    .getCurrentGwtEvent());
 +            Boolean overTreeNode = null;
 +            if (treeNode != null && !treeNode.isLeaf()
 +                    && detail == VerticalDropLocation.MIDDLE) {
 +                overTreeNode = true;
 +            }
 +            drag.getDropDetails().put("itemIdOverIsNode", overTreeNode);
 +            drag.getDropDetails().put("detail", detail);
 +        } else {
 +            drag.getDropDetails().put("itemIdOverIsNode", null);
 +            drag.getDropDetails().put("detail", null);
 +        }
 +
 +    }
 +
 +    private String findCurrentMouseOverKey(Element elementOver) {
 +        TreeNode treeNode = Util.findWidget(elementOver, TreeNode.class);
 +        return treeNode == null ? null : treeNode.key;
 +    }
 +
 +    void updateDropHandler(UIDL childUidl) {
 +        if (dropHandler == null) {
 +            dropHandler = new VAbstractDropHandler() {
 +
 +                @Override
 +                public void dragEnter(VDragEvent drag) {
 +                }
 +
 +                @Override
 +                protected void dragAccepted(final VDragEvent drag) {
 +
 +                }
 +
 +                @Override
 +                public void dragOver(final VDragEvent currentDrag) {
 +                    final Object oldIdOver = currentDrag.getDropDetails().get(
 +                            "itemIdOver");
 +                    final VerticalDropLocation oldDetail = (VerticalDropLocation) currentDrag
 +                            .getDropDetails().get("detail");
 +
 +                    updateTreeRelatedDragData(currentDrag);
 +                    final VerticalDropLocation detail = (VerticalDropLocation) currentDrag
 +                            .getDropDetails().get("detail");
 +                    boolean nodeHasChanged = (currentMouseOverKey != null && currentMouseOverKey != oldIdOver)
 +                            || (currentMouseOverKey == null && oldIdOver != null);
 +                    boolean detailHasChanded = (detail != null && detail != oldDetail)
 +                            || (detail == null && oldDetail != null);
 +
 +                    if (nodeHasChanged || detailHasChanded) {
 +                        final String newKey = currentMouseOverKey;
 +                        TreeNode treeNode = keyToNode.get(oldIdOver);
 +                        if (treeNode != null) {
 +                            // clear old styles
 +                            treeNode.emphasis(null);
 +                        }
 +                        if (newKey != null) {
 +                            validate(new VAcceptCallback() {
 +                                @Override
 +                                public void accepted(VDragEvent event) {
 +                                    VerticalDropLocation curDetail = (VerticalDropLocation) event
 +                                            .getDropDetails().get("detail");
 +                                    if (curDetail == detail
 +                                            && newKey.equals(currentMouseOverKey)) {
 +                                        getNodeByKey(newKey).emphasis(detail);
 +                                    }
 +                                    /*
 +                                     * Else drag is already on a different
 +                                     * node-detail pair, new criteria check is
 +                                     * going on
 +                                     */
 +                                }
 +                            }, currentDrag);
 +
 +                        }
 +                    }
 +
 +                }
 +
 +                @Override
 +                public void dragLeave(VDragEvent drag) {
 +                    cleanUp();
 +                }
 +
 +                private void cleanUp() {
 +                    if (currentMouseOverKey != null) {
 +                        getNodeByKey(currentMouseOverKey).emphasis(null);
 +                        currentMouseOverKey = null;
 +                    }
 +                }
 +
 +                @Override
 +                public boolean drop(VDragEvent drag) {
 +                    cleanUp();
 +                    return super.drop(drag);
 +                }
 +
 +                @Override
 +                public ComponentConnector getConnector() {
 +                    return ConnectorMap.get(client).getConnector(VTree.this);
 +                }
 +
 +                @Override
 +                public ApplicationConnection getApplicationConnection() {
 +                    return client;
 +                }
 +
 +            };
 +        }
 +        dropHandler.updateAcceptRules(childUidl);
 +    }
 +
 +    public void setSelected(TreeNode treeNode, boolean selected) {
 +        if (selected) {
 +            if (!isMultiselect) {
 +                while (selectedIds.size() > 0) {
 +                    final String id = selectedIds.iterator().next();
 +                    final TreeNode oldSelection = getNodeByKey(id);
 +                    if (oldSelection != null) {
 +                        // can be null if the node is not visible (parent
 +                        // collapsed)
 +                        oldSelection.setSelected(false);
 +                    }
 +                    selectedIds.remove(id);
 +                }
 +            }
 +            treeNode.setSelected(true);
 +            selectedIds.add(treeNode.key);
 +        } else {
 +            if (!isNullSelectionAllowed) {
 +                if (!isMultiselect || selectedIds.size() == 1) {
 +                    return;
 +                }
 +            }
 +            selectedIds.remove(treeNode.key);
 +            treeNode.setSelected(false);
 +        }
 +
 +        sendSelectionToServer();
 +    }
 +
 +    /**
 +     * Sends the selection to the server
 +     */
 +    private void sendSelectionToServer() {
 +        Command command = new Command() {
 +            @Override
 +            public void execute() {
 +                client.updateVariable(paintableId, "selected",
 +                        selectedIds.toArray(new String[selectedIds.size()]),
 +                        immediate);
 +                selectionHasChanged = false;
 +            }
 +        };
 +
 +        /*
 +         * Delaying the sending of the selection in webkit to ensure the
 +         * selection is always sent when the tree has focus and after click
 +         * events have been processed. This is due to the focusing
 +         * implementation in FocusImplSafari which uses timeouts when focusing
 +         * and blurring.
 +         */
 +        if (BrowserInfo.get().isWebkit()) {
 +            Scheduler.get().scheduleDeferred(command);
 +        } else {
 +            command.execute();
 +        }
 +    }
 +
 +    /**
 +     * Is a node selected in the tree
 +     * 
 +     * @param treeNode
 +     *            The node to check
 +     * @return
 +     */
 +    public boolean isSelected(TreeNode treeNode) {
 +        return selectedIds.contains(treeNode.key);
 +    }
 +
 +    public class TreeNode extends SimplePanel implements ActionOwner {
 +
 +        public static final String CLASSNAME = "v-tree-node";
 +        public static final String CLASSNAME_FOCUSED = CLASSNAME + "-focused";
 +
 +        public String key;
 +
 +        String[] actionKeys = null;
 +
 +        boolean childrenLoaded;
 +
 +        Element nodeCaptionDiv;
 +
 +        protected Element nodeCaptionSpan;
 +
 +        FlowPanel childNodeContainer;
 +
 +        private boolean open;
 +
 +        private Icon icon;
 +
 +        private Event mouseDownEvent;
 +
 +        private int cachedHeight = -1;
 +
 +        private boolean focused = false;
 +
 +        public TreeNode() {
 +            constructDom();
 +            sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS
 +                    | Event.TOUCHEVENTS | Event.ONCONTEXTMENU);
 +        }
 +
 +        public VerticalDropLocation getDropDetail(NativeEvent currentGwtEvent) {
 +            if (cachedHeight < 0) {
 +                /*
 +                 * Height is cached to avoid flickering (drop hints may change
 +                 * the reported offsetheight -> would change the drop detail)
 +                 */
 +                cachedHeight = nodeCaptionDiv.getOffsetHeight();
 +            }
 +            VerticalDropLocation verticalDropLocation = DDUtil
 +                    .getVerticalDropLocation(nodeCaptionDiv, cachedHeight,
 +                            currentGwtEvent, 0.15);
 +            return verticalDropLocation;
 +        }
 +
 +        protected void emphasis(VerticalDropLocation detail) {
 +            String base = "v-tree-node-drag-";
 +            UIObject.setStyleName(getElement(), base + "top",
 +                    VerticalDropLocation.TOP == detail);
 +            UIObject.setStyleName(getElement(), base + "bottom",
 +                    VerticalDropLocation.BOTTOM == detail);
 +            UIObject.setStyleName(getElement(), base + "center",
 +                    VerticalDropLocation.MIDDLE == detail);
 +            base = "v-tree-node-caption-drag-";
 +            UIObject.setStyleName(nodeCaptionDiv, base + "top",
 +                    VerticalDropLocation.TOP == detail);
 +            UIObject.setStyleName(nodeCaptionDiv, base + "bottom",
 +                    VerticalDropLocation.BOTTOM == detail);
 +            UIObject.setStyleName(nodeCaptionDiv, base + "center",
 +                    VerticalDropLocation.MIDDLE == detail);
 +
 +            // also add classname to "folder node" into which the drag is
 +            // targeted
 +
 +            TreeNode folder = null;
 +            /* Possible parent of this TreeNode will be stored here */
 +            TreeNode parentFolder = getParentNode();
 +
 +            // TODO fix my bugs
 +            if (isLeaf()) {
 +                folder = parentFolder;
 +                // note, parent folder may be null if this is root node => no
 +                // folder target exists
 +            } else {
 +                if (detail == VerticalDropLocation.TOP) {
 +                    folder = parentFolder;
 +                } else {
 +                    folder = this;
 +                }
 +                // ensure we remove the dragfolder classname from the previous
 +                // folder node
 +                setDragFolderStyleName(this, false);
 +                setDragFolderStyleName(parentFolder, false);
 +            }
 +            if (folder != null) {
 +                setDragFolderStyleName(folder, detail != null);
 +            }
 +
 +        }
 +
 +        private TreeNode getParentNode() {
 +            Widget parent2 = getParent().getParent();
 +            if (parent2 instanceof TreeNode) {
 +                return (TreeNode) parent2;
 +            }
 +            return null;
 +        }
 +
 +        private void setDragFolderStyleName(TreeNode folder, boolean add) {
 +            if (folder != null) {
 +                UIObject.setStyleName(folder.getElement(),
 +                        "v-tree-node-dragfolder", add);
 +                UIObject.setStyleName(folder.nodeCaptionDiv,
 +                        "v-tree-node-caption-dragfolder", add);
 +            }
 +        }
 +
 +        /**
 +         * Handles mouse selection
 +         * 
 +         * @param ctrl
 +         *            Was the ctrl-key pressed
 +         * @param shift
 +         *            Was the shift-key pressed
 +         * @return Returns true if event was handled, else false
 +         */
 +        private boolean handleClickSelection(final boolean ctrl,
 +                final boolean shift) {
 +
 +            // always when clicking an item, focus it
 +            setFocusedNode(this, false);
 +
 +            if (!BrowserInfo.get().isOpera()) {
 +                /*
 +                 * Ensure that the tree's focus element also gains focus
 +                 * (TreeNodes focus is faked using FocusElementPanel in browsers
 +                 * other than Opera).
 +                 */
 +                focus();
 +            }
 +
-             };
-             if (BrowserInfo.get().isWebkit() && !treeHasFocus) {
-                 /*
-                  * Safari may need to wait for focus. See FocusImplSafari.
-                  */
-                 // VConsole.log("Deferring click handling to let webkit gain focus...");
-                 Scheduler.get().scheduleDeferred(command);
-             } else {
-                 command.execute();
-             }
++            executeEventCommand(new ScheduledCommand() {
++
 +                @Override
 +                public void execute() {
 +
 +                    if (multiSelectMode == MULTISELECT_MODE_SIMPLE
 +                            || !isMultiselect) {
 +                        toggleSelection();
 +                        lastSelection = TreeNode.this;
 +                    } else if (multiSelectMode == MULTISELECT_MODE_DEFAULT) {
 +                        // Handle ctrl+click
 +                        if (isMultiselect && ctrl && !shift) {
 +                            toggleSelection();
 +                            lastSelection = TreeNode.this;
 +
 +                            // Handle shift+click
 +                        } else if (isMultiselect && !ctrl && shift) {
 +                            deselectAll();
 +                            selectNodeRange(lastSelection.key, key);
 +                            sendSelectionToServer();
 +
 +                            // Handle ctrl+shift click
 +                        } else if (isMultiselect && ctrl && shift) {
 +                            selectNodeRange(lastSelection.key, key);
 +
 +                            // Handle click
 +                        } else {
 +                            // TODO should happen only if this alone not yet
 +                            // selected,
 +                            // now sending excess server calls
 +                            deselectAll();
 +                            toggleSelection();
 +                            lastSelection = TreeNode.this;
 +                        }
 +                    }
 +                }
-                     && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) {
++            });
 +
 +            return true;
 +        }
 +
 +        /*
 +         * (non-Javadoc)
 +         * 
 +         * @see
 +         * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
 +         * .user.client.Event)
 +         */
 +        @Override
 +        public void onBrowserEvent(Event event) {
 +            super.onBrowserEvent(event);
 +            final int type = DOM.eventGetType(event);
 +            final Element target = DOM.eventGetTarget(event);
 +
 +            if (type == Event.ONLOAD && target == icon.getElement()) {
 +                iconLoaded.trigger();
 +            }
 +
 +            if (disabled) {
 +                return;
 +            }
 +
 +            final boolean inCaption = isCaptionElement(target);
 +            if (inCaption
 +                    && client.hasEventListeners(VTree.this,
 +                            TreeConstants.ITEM_CLICK_EVENT_ID)
 +
-                                         .getButton() == NativeEvent.BUTTON_LEFT)) {
++                            && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) {
 +                fireClick(event);
 +            }
 +            if (type == Event.ONCLICK) {
 +                if (getElement() == target) {
 +                    // state change
 +                    toggleState();
 +                } else if (!readonly && inCaption) {
 +                    if (selectable) {
 +                        // caption click = selection change && possible click
 +                        // event
 +                        if (handleClickSelection(
 +                                event.getCtrlKey() || event.getMetaKey(),
 +                                event.getShiftKey())) {
 +                            event.preventDefault();
 +                        }
 +                    } else {
 +                        // Not selectable, only focus the node.
 +                        setFocusedNode(this);
 +                    }
 +                }
 +                event.stopPropagation();
 +            } else if (type == Event.ONCONTEXTMENU) {
 +                showContextMenu(event);
 +            }
 +
 +            if (dragMode != 0 || dropHandler != null) {
 +                if (type == Event.ONMOUSEDOWN || type == Event.ONTOUCHSTART) {
 +                    if (nodeCaptionDiv.isOrHasChild((Node) event
 +                            .getEventTarget().cast())) {
 +                        if (dragMode > 0
 +                                && (type == Event.ONTOUCHSTART || event
-             ScheduledCommand command = new ScheduledCommand() {
++                                .getButton() == NativeEvent.BUTTON_LEFT)) {
 +                            mouseDownEvent = event; // save event for possible
 +                            // dd operation
 +                            if (type == Event.ONMOUSEDOWN) {
 +                                event.preventDefault(); // prevent text
 +                                // selection
 +                            } else {
 +                                /*
 +                                 * FIXME We prevent touch start event to be used
 +                                 * as a scroll start event. Note that we cannot
 +                                 * easily distinguish whether the user wants to
 +                                 * drag or scroll. The same issue is in table
 +                                 * that has scrollable area and has drag and
 +                                 * drop enable. Some kind of timer might be used
 +                                 * to resolve the issue.
 +                                 */
 +                                event.stopPropagation();
 +                            }
 +                        }
 +                    }
 +                } else if (type == Event.ONMOUSEMOVE
 +                        || type == Event.ONMOUSEOUT
 +                        || type == Event.ONTOUCHMOVE) {
 +
 +                    if (mouseDownEvent != null) {
 +                        // start actual drag on slight move when mouse is down
 +                        VTransferable t = new VTransferable();
 +                        t.setDragSource(ConnectorMap.get(client).getConnector(
 +                                VTree.this));
 +                        t.setData("itemId", key);
 +                        VDragEvent drag = VDragAndDropManager.get().startDrag(
 +                                t, mouseDownEvent, true);
 +
 +                        drag.createDragImage(nodeCaptionDiv, true);
 +                        event.stopPropagation();
 +
 +                        mouseDownEvent = null;
 +                    }
 +                } else if (type == Event.ONMOUSEUP) {
 +                    mouseDownEvent = null;
 +                }
 +                if (type == Event.ONMOUSEOVER) {
 +                    mouseDownEvent = null;
 +                    currentMouseOverKey = key;
 +                    event.stopPropagation();
 +                }
 +
 +            } else if (type == Event.ONMOUSEDOWN
 +                    && event.getButton() == NativeEvent.BUTTON_LEFT) {
 +                event.preventDefault(); // text selection
 +            }
 +        }
 +
 +        /**
 +         * Checks if the given element is the caption or the icon.
 +         * 
 +         * @param target
 +         *            The element to check
 +         * @return true if the element is the caption or the icon
 +         */
 +        public boolean isCaptionElement(com.google.gwt.dom.client.Element target) {
 +            return (target == nodeCaptionSpan || (icon != null && target == icon
 +                    .getElement()));
 +        }
 +
 +        private void fireClick(final Event evt) {
 +            /*
 +             * Ensure we have focus in tree before sending variables. Otherwise
 +             * previously modified field may contain dirty variables.
 +             */
 +            if (!treeHasFocus) {
 +                if (BrowserInfo.get().isOpera()) {
 +                    if (focusedNode == null) {
 +                        getNodeByKey(key).setFocused(true);
 +                    } else {
 +                        focusedNode.setFocused(true);
 +                    }
 +                } else {
 +                    focus();
 +                }
 +            }
++
 +            final MouseEventDetails details = MouseEventDetailsBuilder
 +                    .buildMouseEventDetails(evt);
-             };
-             if (treeHasFocus) {
-                 command.execute();
-             } else {
-                 /*
-                  * Webkits need a deferring due to FocusImplSafari uses timeout
-                  */
++
++            executeEventCommand(new ScheduledCommand() {
++
 +                @Override
 +                public void execute() {
 +                    // Determine if we should send the event immediately to the
 +                    // server. We do not want to send the event if there is a
 +                    // selection event happening after this. In all other cases
 +                    // we want to send it immediately.
 +                    boolean sendClickEventNow = true;
 +
 +                    if (details.getButton() == NativeEvent.BUTTON_LEFT
 +                            && immediate && selectable) {
 +                        // Probably a selection that will cause a value change
 +                        // event to be sent
 +                        sendClickEventNow = false;
 +
 +                        // The exception is that user clicked on the
 +                        // currently selected row and null selection is not
 +                        // allowed == no selection event
 +                        if (isSelected() && selectedIds.size() == 1
 +                                && !isNullSelectionAllowed) {
 +                            sendClickEventNow = true;
 +                        }
 +                    }
 +
 +                    client.updateVariable(paintableId, "clickedKey", key, false);
 +                    client.updateVariable(paintableId, "clickEvent",
 +                            details.toString(), sendClickEventNow);
 +                }
-                                 && selectable);
++            });
++        }
++
++        /*
++         * Must wait for Safari to focus before sending click and value change
++         * events (see #6373, #6374)
++         */
++        private void executeEventCommand(ScheduledCommand command) {
++            if (BrowserInfo.get().isWebkit() && !treeHasFocus) {
 +                Scheduler.get().scheduleDeferred(command);
++            } else {
++                command.execute();
 +            }
 +        }
 +
 +        private void toggleSelection() {
 +            if (selectable) {
 +                VTree.this.setSelected(this, !isSelected());
 +            }
 +        }
 +
 +        private void toggleState() {
 +            setState(!getState(), true);
 +        }
 +
 +        protected void constructDom() {
 +            addStyleName(CLASSNAME);
 +
 +            nodeCaptionDiv = DOM.createDiv();
 +            DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME
 +                    + "-caption");
 +            Element wrapper = DOM.createDiv();
 +            nodeCaptionSpan = DOM.createSpan();
 +            DOM.appendChild(getElement(), nodeCaptionDiv);
 +            DOM.appendChild(nodeCaptionDiv, wrapper);
 +            DOM.appendChild(wrapper, nodeCaptionSpan);
 +
 +            if (BrowserInfo.get().isOpera()) {
 +                /*
 +                 * Focus the caption div of the node to get keyboard navigation
 +                 * to work without scrolling up or down when focusing a node.
 +                 */
 +                nodeCaptionDiv.setTabIndex(-1);
 +            }
 +
 +            childNodeContainer = new FlowPanel();
 +            childNodeContainer.setStyleName(CLASSNAME + "-children");
 +            setWidget(childNodeContainer);
 +        }
 +
 +        public boolean isLeaf() {
 +            String[] styleNames = getStyleName().split(" ");
 +            for (String styleName : styleNames) {
 +                if (styleName.equals(CLASSNAME + "-leaf")) {
 +                    return true;
 +                }
 +            }
 +            return false;
 +        }
 +
 +        void setState(boolean state, boolean notifyServer) {
 +            if (open == state) {
 +                return;
 +            }
 +            if (state) {
 +                if (!childrenLoaded && notifyServer) {
 +                    client.updateVariable(paintableId, "requestChildTree",
 +                            true, false);
 +                }
 +                if (notifyServer) {
 +                    client.updateVariable(paintableId, "expand",
 +                            new String[] { key }, true);
 +                }
 +                addStyleName(CLASSNAME + "-expanded");
 +                childNodeContainer.setVisible(true);
 +
 +            } else {
 +                removeStyleName(CLASSNAME + "-expanded");
 +                childNodeContainer.setVisible(false);
 +                if (notifyServer) {
 +                    client.updateVariable(paintableId, "collapse",
 +                            new String[] { key }, true);
 +                }
 +            }
 +            open = state;
 +
 +            if (!rendering) {
 +                Util.notifyParentOfSizeChange(VTree.this, false);
 +            }
 +        }
 +
 +        boolean getState() {
 +            return open;
 +        }
 +
 +        void setText(String text) {
 +            DOM.setInnerText(nodeCaptionSpan, text);
 +        }
 +
 +        public boolean isChildrenLoaded() {
 +            return childrenLoaded;
 +        }
 +
 +        /**
 +         * Returns the children of the node
 +         * 
 +         * @return A set of tree nodes
 +         */
 +        public List<TreeNode> getChildren() {
 +            List<TreeNode> nodes = new LinkedList<TreeNode>();
 +
 +            if (!isLeaf() && isChildrenLoaded()) {
 +                Iterator<Widget> iter = childNodeContainer.iterator();
 +                while (iter.hasNext()) {
 +                    TreeNode node = (TreeNode) iter.next();
 +                    nodes.add(node);
 +                }
 +            }
 +            return nodes;
 +        }
 +
 +        @Override
 +        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(key),
 +                        actionKey);
 +                a.setCaption(getActionCaption(actionKey));
 +                a.setIconUrl(getActionIcon(actionKey));
 +                actions[i] = a;
 +            }
 +            return actions;
 +        }
 +
 +        @Override
 +        public ApplicationConnection getClient() {
 +            return client;
 +        }
 +
 +        @Override
 +        public String getPaintableId() {
 +            return paintableId;
 +        }
 +
 +        /**
 +         * Adds/removes Vaadin specific style name. This method ought to be
 +         * called only from VTree.
 +         * 
 +         * @param selected
 +         */
 +        protected void setSelected(boolean selected) {
 +            // add style name to caption dom structure only, not to subtree
 +            setStyleName(nodeCaptionDiv, "v-tree-node-selected", selected);
 +        }
 +
 +        protected boolean isSelected() {
 +            return VTree.this.isSelected(this);
 +        }
 +
 +        /**
 +         * Travels up the hierarchy looking for this node
 +         * 
 +         * @param child
 +         *            The child which grandparent this is or is not
 +         * @return True if this is a grandparent of the child node
 +         */
 +        public boolean isGrandParentOf(TreeNode child) {
 +            TreeNode currentNode = child;
 +            boolean isGrandParent = false;
 +            while (currentNode != null) {
 +                currentNode = currentNode.getParentNode();
 +                if (currentNode == this) {
 +                    isGrandParent = true;
 +                    break;
 +                }
 +            }
 +            return isGrandParent;
 +        }
 +
 +        public boolean isSibling(TreeNode node) {
 +            return node.getParentNode() == getParentNode();
 +        }
 +
 +        public void showContextMenu(Event event) {
 +            if (!readonly && !disabled) {
 +                if (actionKeys != null) {
 +                    int left = event.getClientX();
 +                    int top = event.getClientY();
 +                    top += Window.getScrollTop();
 +                    left += Window.getScrollLeft();
 +                    client.getContextMenu().showAt(this, left, top);
 +                }
 +                event.stopPropagation();
 +                event.preventDefault();
 +            }
 +        }
 +
 +        /*
 +         * (non-Javadoc)
 +         * 
 +         * @see com.google.gwt.user.client.ui.Widget#onDetach()
 +         */
 +        @Override
 +        protected void onDetach() {
 +            super.onDetach();
 +            client.getContextMenu().ensureHidden(this);
 +        }
 +
 +        /*
 +         * (non-Javadoc)
 +         * 
 +         * @see com.google.gwt.user.client.ui.UIObject#toString()
 +         */
 +        @Override
 +        public String toString() {
 +            return nodeCaptionSpan.getInnerText();
 +        }
 +
 +        /**
 +         * Is the node focused?
 +         * 
 +         * @param focused
 +         *            True if focused, false if not
 +         */
 +        public void setFocused(boolean focused) {
 +            if (!this.focused && focused) {
 +                nodeCaptionDiv.addClassName(CLASSNAME_FOCUSED);
 +
 +                this.focused = focused;
 +                if (BrowserInfo.get().isOpera()) {
 +                    nodeCaptionDiv.focus();
 +                }
 +                treeHasFocus = true;
 +            } else if (this.focused && !focused) {
 +                nodeCaptionDiv.removeClassName(CLASSNAME_FOCUSED);
 +                this.focused = focused;
 +                treeHasFocus = false;
 +            }
 +        }
 +
 +        /**
 +         * Scrolls the caption into view
 +         */
 +        public void scrollIntoView() {
 +            Util.scrollIntoViewVertically(nodeCaptionDiv);
 +        }
 +
 +        public void setIcon(String iconUrl) {
 +            if (iconUrl != null) {
 +                // Add icon if not present
 +                if (icon == null) {
 +                    icon = new Icon(client);
 +                    DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv),
 +                            icon.getElement(), nodeCaptionSpan);
 +                }
 +                icon.setUri(iconUrl);
 +            } else {
 +                // Remove icon if present
 +                if (icon != null) {
 +                    DOM.removeChild(DOM.getFirstChild(nodeCaptionDiv),
 +                            icon.getElement());
 +                    icon = null;
 +                }
 +            }
 +        }
 +
 +        public void setNodeStyleName(String styleName) {
 +            addStyleName(TreeNode.CLASSNAME + "-" + styleName);
 +            setStyleName(nodeCaptionDiv, TreeNode.CLASSNAME + "-caption-"
 +                    + styleName, true);
 +            childNodeContainer.addStyleName(TreeNode.CLASSNAME + "-children-"
 +                    + styleName);
 +
 +        }
 +
 +    }
 +
 +    @Override
 +    public VDropHandler getDropHandler() {
 +        return dropHandler;
 +    }
 +
 +    public TreeNode getNodeByKey(String key) {
 +        return keyToNode.get(key);
 +    }
 +
 +    /**
 +     * Deselects all items in the tree
 +     */
 +    public void deselectAll() {
 +        for (String key : selectedIds) {
 +            TreeNode node = keyToNode.get(key);
 +            if (node != null) {
 +                node.setSelected(false);
 +            }
 +        }
 +        selectedIds.clear();
 +        selectionHasChanged = true;
 +    }
 +
 +    /**
 +     * Selects a range of nodes
 +     * 
 +     * @param startNodeKey
 +     *            The start node key
 +     * @param endNodeKey
 +     *            The end node key
 +     */
 +    private void selectNodeRange(String startNodeKey, String endNodeKey) {
 +
 +        TreeNode startNode = keyToNode.get(startNodeKey);
 +        TreeNode endNode = keyToNode.get(endNodeKey);
 +
 +        // The nodes have the same parent
 +        if (startNode.getParent() == endNode.getParent()) {
 +            doSiblingSelection(startNode, endNode);
 +
 +            // The start node is a grandparent of the end node
 +        } else if (startNode.isGrandParentOf(endNode)) {
 +            doRelationSelection(startNode, endNode);
 +
 +            // The end node is a grandparent of the start node
 +        } else if (endNode.isGrandParentOf(startNode)) {
 +            doRelationSelection(endNode, startNode);
 +
 +        } else {
 +            doNoRelationSelection(startNode, endNode);
 +        }
 +    }
 +
 +    /**
 +     * Selects a node and deselect all other nodes
 +     * 
 +     * @param node
 +     *            The node to select
 +     */
 +    private void selectNode(TreeNode node, boolean deselectPrevious) {
 +        if (deselectPrevious) {
 +            deselectAll();
 +        }
 +
 +        if (node != null) {
 +            node.setSelected(true);
 +            selectedIds.add(node.key);
 +            lastSelection = node;
 +        }
 +        selectionHasChanged = true;
 +    }
 +
 +    /**
 +     * Deselects a node
 +     * 
 +     * @param node
 +     *            The node to deselect
 +     */
 +    private void deselectNode(TreeNode node) {
 +        node.setSelected(false);
 +        selectedIds.remove(node.key);
 +        selectionHasChanged = true;
 +    }
 +
 +    /**
 +     * Selects all the open children to a node
 +     * 
 +     * @param node
 +     *            The parent node
 +     */
 +    private void selectAllChildren(TreeNode node, boolean includeRootNode) {
 +        if (includeRootNode) {
 +            node.setSelected(true);
 +            selectedIds.add(node.key);
 +        }
 +
 +        for (TreeNode child : node.getChildren()) {
 +            if (!child.isLeaf() && child.getState()) {
 +                selectAllChildren(child, true);
 +            } else {
 +                child.setSelected(true);
 +                selectedIds.add(child.key);
 +            }
 +        }
 +        selectionHasChanged = true;
 +    }
 +
 +    /**
 +     * Selects all children until a stop child is reached
 +     * 
 +     * @param root
 +     *            The root not to start from
 +     * @param stopNode
 +     *            The node to finish with
 +     * @param includeRootNode
 +     *            Should the root node be selected
 +     * @param includeStopNode
 +     *            Should the stop node be selected
 +     * 
 +     * @return Returns false if the stop child was found, else true if all
 +     *         children was selected
 +     */
 +    private boolean selectAllChildrenUntil(TreeNode root, TreeNode stopNode,
 +            boolean includeRootNode, boolean includeStopNode) {
 +        if (includeRootNode) {
 +            root.setSelected(true);
 +            selectedIds.add(root.key);
 +        }
 +        if (root.getState() && root != stopNode) {
 +            for (TreeNode child : root.getChildren()) {
 +                if (!child.isLeaf() && child.getState() && child != stopNode) {
 +                    if (!selectAllChildrenUntil(child, stopNode, true,
 +                            includeStopNode)) {
 +                        return false;
 +                    }
 +                } else if (child == stopNode) {
 +                    if (includeStopNode) {
 +                        child.setSelected(true);
 +                        selectedIds.add(child.key);
 +                    }
 +                    return false;
 +                } else {
 +                    child.setSelected(true);
 +                    selectedIds.add(child.key);
 +                }
 +            }
 +        }
 +        selectionHasChanged = true;
 +
 +        return true;
 +    }
 +
 +    /**
 +     * Select a range between two nodes which have no relation to each other
 +     * 
 +     * @param startNode
 +     *            The start node to start the selection from
 +     * @param endNode
 +     *            The end node to end the selection to
 +     */
 +    private void doNoRelationSelection(TreeNode startNode, TreeNode endNode) {
 +
 +        TreeNode commonParent = getCommonGrandParent(startNode, endNode);
 +        TreeNode startBranch = null, endBranch = null;
 +
 +        // Find the children of the common parent
 +        List<TreeNode> children;
 +        if (commonParent != null) {
 +            children = commonParent.getChildren();
 +        } else {
 +            children = getRootNodes();
 +        }
 +
 +        // Find the start and end branches
 +        for (TreeNode node : children) {
 +            if (nodeIsInBranch(startNode, node)) {
 +                startBranch = node;
 +            }
 +            if (nodeIsInBranch(endNode, node)) {
 +                endBranch = node;
 +            }
 +        }
 +
 +        // Swap nodes if necessary
 +        if (children.indexOf(startBranch) > children.indexOf(endBranch)) {
 +            TreeNode temp = startBranch;
 +            startBranch = endBranch;
 +            endBranch = temp;
 +
 +            temp = startNode;
 +            startNode = endNode;
 +            endNode = temp;
 +        }
 +
 +        // Select all children under the start node
 +        selectAllChildren(startNode, true);
 +        TreeNode startParent = startNode.getParentNode();
 +        TreeNode currentNode = startNode;
 +        while (startParent != null && startParent != commonParent) {
 +            List<TreeNode> startChildren = startParent.getChildren();
 +            for (int i = startChildren.indexOf(currentNode) + 1; i < startChildren
 +                    .size(); i++) {
 +                selectAllChildren(startChildren.get(i), true);
 +            }
 +
 +            currentNode = startParent;
 +            startParent = startParent.getParentNode();
 +        }
 +
 +        // Select nodes until the end node is reached
 +        for (int i = children.indexOf(startBranch) + 1; i <= children
 +                .indexOf(endBranch); i++) {
 +            selectAllChildrenUntil(children.get(i), endNode, true, true);
 +        }
 +
 +        // Ensure end node was selected
 +        endNode.setSelected(true);
 +        selectedIds.add(endNode.key);
 +        selectionHasChanged = true;
 +    }
 +
 +    /**
 +     * Examines the children of the branch node and returns true if a node is in
 +     * that branch
 +     * 
 +     * @param node
 +     *            The node to search for
 +     * @param branch
 +     *            The branch to search in
 +     * @return True if found, false if not found
 +     */
 +    private boolean nodeIsInBranch(TreeNode node, TreeNode branch) {
 +        if (node == branch) {
 +            return true;
 +        }
 +        for (TreeNode child : branch.getChildren()) {
 +            if (child == node) {
 +                return true;
 +            }
 +            if (!child.isLeaf() && child.getState()) {
 +                if (nodeIsInBranch(node, child)) {
 +                    return true;
 +                }
 +            }
 +        }
 +        return false;
 +    }
 +
 +    /**
 +     * Selects a range of items which are in direct relation with each other.<br/>
 +     * NOTE: The start node <b>MUST</b> be before the end node!
 +     * 
 +     * @param startNode
 +     * 
 +     * @param endNode
 +     */
 +    private void doRelationSelection(TreeNode startNode, TreeNode endNode) {
 +        TreeNode currentNode = endNode;
 +        while (currentNode != startNode) {
 +            currentNode.setSelected(true);
 +            selectedIds.add(currentNode.key);
 +
 +            // Traverse children above the selection
 +            List<TreeNode> subChildren = currentNode.getParentNode()
 +                    .getChildren();
 +            if (subChildren.size() > 1) {
 +                selectNodeRange(subChildren.iterator().next().key,
 +                        currentNode.key);
 +            } else if (subChildren.size() == 1) {
 +                TreeNode n = subChildren.get(0);
 +                n.setSelected(true);
 +                selectedIds.add(n.key);
 +            }
 +
 +            currentNode = currentNode.getParentNode();
 +        }
 +        startNode.setSelected(true);
 +        selectedIds.add(startNode.key);
 +        selectionHasChanged = true;
 +    }
 +
 +    /**
 +     * Selects a range of items which have the same parent.
 +     * 
 +     * @param startNode
 +     *            The start node
 +     * @param endNode
 +     *            The end node
 +     */
 +    private void doSiblingSelection(TreeNode startNode, TreeNode endNode) {
 +        TreeNode parent = startNode.getParentNode();
 +
 +        List<TreeNode> children;
 +        if (parent == null) {
 +            // Topmost parent
 +            children = getRootNodes();
 +        } else {
 +            children = parent.getChildren();
 +        }
 +
 +        // Swap start and end point if needed
 +        if (children.indexOf(startNode) > children.indexOf(endNode)) {
 +            TreeNode temp = startNode;
 +            startNode = endNode;
 +            endNode = temp;
 +        }
 +
 +        Iterator<TreeNode> childIter = children.iterator();
 +        boolean startFound = false;
 +        while (childIter.hasNext()) {
 +            TreeNode node = childIter.next();
 +            if (node == startNode) {
 +                startFound = true;
 +            }
 +
 +            if (startFound && node != endNode && node.getState()) {
 +                selectAllChildren(node, true);
 +            } else if (startFound && node != endNode) {
 +                node.setSelected(true);
 +                selectedIds.add(node.key);
 +            }
 +
 +            if (node == endNode) {
 +                node.setSelected(true);
 +                selectedIds.add(node.key);
 +                break;
 +            }
 +        }
 +        selectionHasChanged = true;
 +    }
 +
 +    /**
 +     * Returns the first common parent of two nodes
 +     * 
 +     * @param node1
 +     *            The first node
 +     * @param node2
 +     *            The second node
 +     * @return The common parent or null
 +     */
 +    public TreeNode getCommonGrandParent(TreeNode node1, TreeNode node2) {
 +        // If either one does not have a grand parent then return null
 +        if (node1.getParentNode() == null || node2.getParentNode() == null) {
 +            return null;
 +        }
 +
 +        // If the nodes are parents of each other then return null
 +        if (node1.isGrandParentOf(node2) || node2.isGrandParentOf(node1)) {
 +            return null;
 +        }
 +
 +        // Get parents of node1
 +        List<TreeNode> parents1 = new ArrayList<TreeNode>();
 +        TreeNode parent1 = node1.getParentNode();
 +        while (parent1 != null) {
 +            parents1.add(parent1);
 +            parent1 = parent1.getParentNode();
 +        }
 +
 +        // Get parents of node2
 +        List<TreeNode> parents2 = new ArrayList<TreeNode>();
 +        TreeNode parent2 = node2.getParentNode();
 +        while (parent2 != null) {
 +            parents2.add(parent2);
 +            parent2 = parent2.getParentNode();
 +        }
 +
 +        // Search the parents for the first common parent
 +        for (int i = 0; i < parents1.size(); i++) {
 +            parent1 = parents1.get(i);
 +            for (int j = 0; j < parents2.size(); j++) {
 +                parent2 = parents2.get(j);
 +                if (parent1 == parent2) {
 +                    return parent1;
 +                }
 +            }
 +        }
 +
 +        return null;
 +    }
 +
 +    /**
 +     * Sets the node currently in focus
 +     * 
 +     * @param node
 +     *            The node to focus or null to remove the focus completely
 +     * @param scrollIntoView
 +     *            Scroll the node into view
 +     */
 +    public void setFocusedNode(TreeNode node, boolean scrollIntoView) {
 +        // Unfocus previously focused node
 +        if (focusedNode != null) {
 +            focusedNode.setFocused(false);
 +        }
 +
 +        if (node != null) {
 +            node.setFocused(true);
 +        }
 +
 +        focusedNode = node;
 +
 +        if (node != null && scrollIntoView) {
 +            /*
 +             * Delay scrolling the focused node into view if we are still
 +             * rendering. #5396
 +             */
 +            if (!rendering) {
 +                node.scrollIntoView();
 +            } else {
 +                Scheduler.get().scheduleDeferred(new Command() {
 +                    @Override
 +                    public void execute() {
 +                        focusedNode.scrollIntoView();
 +                    }
 +                });
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Focuses a node and scrolls it into view
 +     * 
 +     * @param node
 +     *            The node to focus
 +     */
 +    public void setFocusedNode(TreeNode node) {
 +        setFocusedNode(node, true);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
 +     * .dom.client.FocusEvent)
 +     */
 +    @Override
 +    public void onFocus(FocusEvent event) {
 +        treeHasFocus = true;
 +        // If no node has focus, focus the first item in the tree
 +        if (focusedNode == null && lastSelection == null && selectable) {
 +            setFocusedNode(getFirstRootNode(), false);
 +        } else if (focusedNode != null && selectable) {
 +            setFocusedNode(focusedNode, false);
 +        } else if (lastSelection != null && selectable) {
 +            setFocusedNode(lastSelection, false);
 +        }
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
 +     * .dom.client.BlurEvent)
 +     */
 +    @Override
 +    public void onBlur(BlurEvent event) {
 +        treeHasFocus = false;
 +        if (focusedNode != null) {
 +            focusedNode.setFocused(false);
 +        }
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
 +     * .gwt.event.dom.client.KeyPressEvent)
 +     */
 +    @Override
 +    public void onKeyPress(KeyPressEvent event) {
 +        NativeEvent nativeEvent = event.getNativeEvent();
 +        int keyCode = nativeEvent.getKeyCode();
 +        if (keyCode == 0 && nativeEvent.getCharCode() == ' ') {
 +            // Provide a keyCode for space to be compatible with FireFox
 +            // keypress event
 +            keyCode = CHARCODE_SPACE;
 +        }
 +        if (handleKeyNavigation(keyCode,
 +                event.isControlKeyDown() || event.isMetaKeyDown(),
 +                event.isShiftKeyDown())) {
 +            event.preventDefault();
 +            event.stopPropagation();
 +        }
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
 +     * .event.dom.client.KeyDownEvent)
 +     */
 +    @Override
 +    public void onKeyDown(KeyDownEvent event) {
 +        if (handleKeyNavigation(event.getNativeEvent().getKeyCode(),
 +                event.isControlKeyDown() || event.isMetaKeyDown(),
 +                event.isShiftKeyDown())) {
 +            event.preventDefault();
 +            event.stopPropagation();
 +        }
 +    }
 +
 +    /**
 +     * Handles the keyboard navigation
 +     * 
 +     * @param keycode
 +     *            The keycode of the pressed key
 +     * @param ctrl
 +     *            Was ctrl pressed
 +     * @param shift
 +     *            Was shift pressed
 +     * @return Returns true if the key was handled, else false
 +     */
 +    protected boolean handleKeyNavigation(int keycode, boolean ctrl,
 +            boolean shift) {
 +        // Navigate down
 +        if (keycode == getNavigationDownKey()) {
 +            TreeNode node = null;
 +            // If node is open and has children then move in to the children
 +            if (!focusedNode.isLeaf() && focusedNode.getState()
 +                    && focusedNode.getChildren().size() > 0) {
 +                node = focusedNode.getChildren().get(0);
 +            }
 +
 +            // Else move down to the next sibling
 +            else {
 +                node = getNextSibling(focusedNode);
 +                if (node == null) {
 +                    // Else jump to the parent and try to select the next
 +                    // sibling there
 +                    TreeNode current = focusedNode;
 +                    while (node == null && current.getParentNode() != null) {
 +                        node = getNextSibling(current.getParentNode());
 +                        current = current.getParentNode();
 +                    }
 +                }
 +            }
 +
 +            if (node != null) {
 +                setFocusedNode(node);
 +                if (selectable) {
 +                    if (!ctrl && !shift) {
 +                        selectNode(node, true);
 +                    } else if (shift && isMultiselect) {
 +                        deselectAll();
 +                        selectNodeRange(lastSelection.key, node.key);
 +                    } else if (shift) {
 +                        selectNode(node, true);
 +                    }
 +                }
 +            }
 +            return true;
 +        }
 +
 +        // Navigate up
 +        if (keycode == getNavigationUpKey()) {
 +            TreeNode prev = getPreviousSibling(focusedNode);
 +            TreeNode node = null;
 +            if (prev != null) {
 +                node = getLastVisibleChildInTree(prev);
 +            } else if (focusedNode.getParentNode() != null) {
 +                node = focusedNode.getParentNode();
 +            }
 +            if (node != null) {
 +                setFocusedNode(node);
 +                if (selectable) {
 +                    if (!ctrl && !shift) {
 +                        selectNode(node, true);
 +                    } else if (shift && isMultiselect) {
 +                        deselectAll();
 +                        selectNodeRange(lastSelection.key, node.key);
 +                    } else if (shift) {
 +                        selectNode(node, true);
 +                    }
 +                }
 +            }
 +            return true;
 +        }
 +
 +        // Navigate left (close branch)
 +        if (keycode == getNavigationLeftKey()) {
 +            if (!focusedNode.isLeaf() && focusedNode.getState()) {
 +                focusedNode.setState(false, true);
 +            } else if (focusedNode.getParentNode() != null
 +                    && (focusedNode.isLeaf() || !focusedNode.getState())) {
 +
 +                if (ctrl || !selectable) {
 +                    setFocusedNode(focusedNode.getParentNode());
 +                } else if (shift) {
 +                    doRelationSelection(focusedNode.getParentNode(),
 +                            focusedNode);
 +                    setFocusedNode(focusedNode.getParentNode());
 +                } else {
 +                    focusAndSelectNode(focusedNode.getParentNode());
 +                }
 +            }
 +            return true;
 +        }
 +
 +        // Navigate right (open branch)
 +        if (keycode == getNavigationRightKey()) {
 +            if (!focusedNode.isLeaf() && !focusedNode.getState()) {
 +                focusedNode.setState(true, true);
 +            } else if (!focusedNode.isLeaf()) {
 +                if (ctrl || !selectable) {
 +                    setFocusedNode(focusedNode.getChildren().get(0));
 +                } else if (shift) {
 +                    setSelected(focusedNode, true);
 +                    setFocusedNode(focusedNode.getChildren().get(0));
 +                    setSelected(focusedNode, true);
 +                } else {
 +                    focusAndSelectNode(focusedNode.getChildren().get(0));
 +                }
 +            }
 +            return true;
 +        }
 +
 +        // Selection
 +        if (keycode == getNavigationSelectKey()) {
 +            if (!focusedNode.isSelected()) {
 +                selectNode(
 +                        focusedNode,
 +                        (!isMultiselect || multiSelectMode == MULTISELECT_MODE_SIMPLE)
++                        && selectable);
 +            } else {
 +                deselectNode(focusedNode);
 +            }
 +            return true;
 +        }
 +
 +        // Home selection
 +        if (keycode == getNavigationStartKey()) {
 +            TreeNode node = getFirstRootNode();
 +            if (ctrl || !selectable) {
 +                setFocusedNode(node);
 +            } else if (shift) {
 +                deselectAll();
 +                selectNodeRange(focusedNode.key, node.key);
 +            } else {
 +                selectNode(node, true);
 +            }
 +            sendSelectionToServer();
 +            return true;
 +        }
 +
 +        // End selection
 +        if (keycode == getNavigationEndKey()) {
 +            TreeNode lastNode = getLastRootNode();
 +            TreeNode node = getLastVisibleChildInTree(lastNode);
 +            if (ctrl || !selectable) {
 +                setFocusedNode(node);
 +            } else if (shift) {
 +                deselectAll();
 +                selectNodeRange(focusedNode.key, node.key);
 +            } else {
 +                selectNode(node, true);
 +            }
 +            sendSelectionToServer();
 +            return true;
 +        }
 +
 +        return false;
 +    }
 +
 +    private void focusAndSelectNode(TreeNode node) {
 +        /*
 +         * Keyboard navigation doesn't work reliably if the tree is in
 +         * multiselect mode as well as isNullSelectionAllowed = false. It first
 +         * tries to deselect the old focused node, which fails since there must
 +         * be at least one selection. After this the newly focused node is
 +         * selected and we've ended up with two selected nodes even though we
 +         * only navigated with the arrow keys.
 +         * 
 +         * Because of this, we first select the next node and later de-select
 +         * the old one.
 +         */
 +        TreeNode oldFocusedNode = focusedNode;
 +        setFocusedNode(node);
 +        setSelected(focusedNode, true);
 +        setSelected(oldFocusedNode, false);
 +    }
 +
 +    /**
 +     * Traverses the tree to the bottom most child
 +     * 
 +     * @param root
 +     *            The root of the tree
 +     * @return The bottom most child
 +     */
 +    private TreeNode getLastVisibleChildInTree(TreeNode root) {
 +        if (root.isLeaf() || !root.getState() || root.getChildren().size() == 0) {
 +            return root;
 +        }
 +        List<TreeNode> children = root.getChildren();
 +        return getLastVisibleChildInTree(children.get(children.size() - 1));
 +    }
 +
 +    /**
 +     * Gets the next sibling in the tree
 +     * 
 +     * @param node
 +     *            The node to get the sibling for
 +     * @return The sibling node or null if the node is the last sibling
 +     */
 +    private TreeNode getNextSibling(TreeNode node) {
 +        TreeNode parent = node.getParentNode();
 +        List<TreeNode> children;
 +        if (parent == null) {
 +            children = getRootNodes();
 +        } else {
 +            children = parent.getChildren();
 +        }
 +
 +        int idx = children.indexOf(node);
 +        if (idx < children.size() - 1) {
 +            return children.get(idx + 1);
 +        }
 +
 +        return null;
 +    }
 +
 +    /**
 +     * Returns the previous sibling in the tree
 +     * 
 +     * @param node
 +     *            The node to get the sibling for
 +     * @return The sibling node or null if the node is the first sibling
 +     */
 +    private TreeNode getPreviousSibling(TreeNode node) {
 +        TreeNode parent = node.getParentNode();
 +        List<TreeNode> children;
 +        if (parent == null) {
 +            children = getRootNodes();
 +        } else {
 +            children = parent.getChildren();
 +        }
 +
 +        int idx = children.indexOf(node);
 +        if (idx > 0) {
 +            return children.get(idx - 1);
 +        }
 +
 +        return null;
 +    }
 +
 +    /**
 +     * 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 native JavaScriptObject applyDisableTextSelectionIEHack()
 +    /*-{
 +            return function(){ return false; };
 +    }-*/;
 +
 +    /**
 +     * 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;
 +    }
 +
 +    private final String SUBPART_NODE_PREFIX = "n";
 +    private final String EXPAND_IDENTIFIER = "expand";
 +
 +    /*
 +     * In webkit, focus may have been requested for this component but not yet
 +     * gained. Use this to trac if tree has gained the focus on webkit. See
 +     * FocusImplSafari and #6373
 +     */
 +    private boolean treeHasFocus;
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.vaadin.terminal.gwt.client.ui.SubPartAware#getSubPartElement(java
 +     * .lang.String)
 +     */
 +    @Override
 +    public Element getSubPartElement(String subPart) {
 +        if ("fe".equals(subPart)) {
 +            if (BrowserInfo.get().isOpera() && focusedNode != null) {
 +                return focusedNode.getElement();
 +            }
 +            return getFocusElement();
 +        }
 +
 +        if (subPart.startsWith(SUBPART_NODE_PREFIX + "[")) {
 +            boolean expandCollapse = false;
 +
 +            // Node
 +            String[] nodes = subPart.split("/");
 +            TreeNode treeNode = null;
 +            try {
 +                for (String node : nodes) {
 +                    if (node.startsWith(SUBPART_NODE_PREFIX)) {
 +
 +                        // skip SUBPART_NODE_PREFIX"["
 +                        node = node.substring(SUBPART_NODE_PREFIX.length() + 1);
 +                        // skip "]"
 +                        node = node.substring(0, node.length() - 1);
 +                        int position = Integer.parseInt(node);
 +                        if (treeNode == null) {
 +                            treeNode = getRootNodes().get(position);
 +                        } else {
 +                            treeNode = treeNode.getChildren().get(position);
 +                        }
 +                    } else if (node.startsWith(EXPAND_IDENTIFIER)) {
 +                        expandCollapse = true;
 +                    }
 +                }
 +
 +                if (expandCollapse) {
 +                    return treeNode.getElement();
 +                } else {
 +                    return treeNode.nodeCaptionSpan;
 +                }
 +            } catch (Exception e) {
 +                // Invalid locator string or node could not be found
 +                return null;
 +            }
 +        }
 +        return null;
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.vaadin.terminal.gwt.client.ui.SubPartAware#getSubPartName(com.google
 +     * .gwt.user.client.Element)
 +     */
 +    @Override
 +    public String getSubPartName(Element subElement) {
 +        // Supported identifiers:
 +        //
 +        // n[index]/n[index]/n[index]{/expand}
 +        //
 +        // Ends with "/expand" if the target is expand/collapse indicator,
 +        // otherwise ends with the node
 +
 +        boolean isExpandCollapse = false;
 +
 +        if (!getElement().isOrHasChild(subElement)) {
 +            return null;
 +        }
 +
 +        if (subElement == getFocusElement()) {
 +            return "fe";
 +        }
 +
 +        TreeNode treeNode = Util.findWidget(subElement, TreeNode.class);
 +        if (treeNode == null) {
 +            // Did not click on a node, let somebody else take care of the
 +            // locator string
 +            return null;
 +        }
 +
 +        if (subElement == treeNode.getElement()) {
 +            // Targets expand/collapse arrow
 +            isExpandCollapse = true;
 +        }
 +
 +        ArrayList<Integer> positions = new ArrayList<Integer>();
 +        while (treeNode.getParentNode() != null) {
 +            positions.add(0,
 +                    treeNode.getParentNode().getChildren().indexOf(treeNode));
 +            treeNode = treeNode.getParentNode();
 +        }
 +        positions.add(0, getRootNodes().indexOf(treeNode));
 +
 +        String locator = "";
 +        for (Integer i : positions) {
 +            locator += SUBPART_NODE_PREFIX + "[" + i + "]/";
 +        }
 +
 +        locator = locator.substring(0, locator.length() - 1);
 +        if (isExpandCollapse) {
 +            locator += "/" + EXPAND_IDENTIFIER;
 +        }
 +        return locator;
 +    }
 +
 +    @Override
 +    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];
 +            final TreeAction a = new TreeAction(this, null, actionKey);
 +            a.setCaption(getActionCaption(actionKey));
 +            a.setIconUrl(getActionIcon(actionKey));
 +            actions[i] = a;
 +        }
 +        return actions;
 +    }
 +
 +    @Override
 +    public ApplicationConnection getClient() {
 +        return client;
 +    }
 +
 +    @Override
 +    public String getPaintableId() {
 +        return paintableId;
 +    }
 +
 +    private void handleBodyContextMenu(ContextMenuEvent event) {
 +        if (!readonly && !disabled) {
 +            if (bodyActionKeys != null) {
 +                int left = event.getNativeEvent().getClientX();
 +                int top = event.getNativeEvent().getClientY();
 +                top += Window.getScrollTop();
 +                left += Window.getScrollLeft();
 +                client.getContextMenu().showAt(this, left, top);
 +            }
 +            event.stopPropagation();
 +            event.preventDefault();
 +        }
 +    }
 +
 +    public void registerAction(String key, String caption, String iconUrl) {
 +        actionMap.put(key + "_c", caption);
 +        if (iconUrl != null) {
 +            actionMap.put(key + "_i", iconUrl);
 +        } else {
 +            actionMap.remove(key + "_i");
 +        }
 +
 +    }
 +
 +    public void registerNode(TreeNode treeNode) {
 +        keyToNode.put(treeNode.key, treeNode);
 +    }
 +
 +    public void clearNodeToKeyMap() {
 +        keyToNode.clear();
 +    }
 +
 +}
index a8621190aeaa923d25666f7ce0fbc146378fb4e1,0000000000000000000000000000000000000000..909acdf85f7dc4875b83ae1824e421c01984335d
mode 100644,000000..100644
--- /dev/null
@@@ -1,842 -1,0 +1,865 @@@
-                 return curColIndex == colIndexOfHierarchy
-                         + (showRowHeaders ? 1 : 0);
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +
 +package com.vaadin.terminal.gwt.client.ui.treetable;
 +
 +import java.util.ArrayList;
 +import java.util.Iterator;
 +import java.util.LinkedList;
 +import java.util.List;
 +
 +import com.google.gwt.animation.client.Animation;
 +import com.google.gwt.core.client.Scheduler;
 +import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 +import com.google.gwt.dom.client.Document;
 +import com.google.gwt.dom.client.ImageElement;
 +import com.google.gwt.dom.client.SpanElement;
 +import com.google.gwt.dom.client.Style.Display;
 +import com.google.gwt.dom.client.Style.Unit;
 +import com.google.gwt.dom.client.Style.Visibility;
 +import com.google.gwt.dom.client.TableCellElement;
 +import com.google.gwt.event.dom.client.KeyCodes;
 +import com.google.gwt.user.client.DOM;
 +import com.google.gwt.user.client.Element;
 +import com.google.gwt.user.client.Event;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.terminal.gwt.client.ComputedStyle;
 +import com.vaadin.terminal.gwt.client.UIDL;
 +import com.vaadin.terminal.gwt.client.Util;
 +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable;
 +import com.vaadin.terminal.gwt.client.ui.treetable.VTreeTable.VTreeTableScrollBody.VTreeTableRow;
 +
 +public class VTreeTable extends VScrollTable {
 +
 +    static class PendingNavigationEvent {
 +        final int keycode;
 +        final boolean ctrl;
 +        final boolean shift;
 +
 +        public PendingNavigationEvent(int keycode, boolean ctrl, boolean shift) {
 +            this.keycode = keycode;
 +            this.ctrl = ctrl;
 +            this.shift = shift;
 +        }
 +
 +        @Override
 +        public String toString() {
 +            String string = "Keyboard event: " + keycode;
 +            if (ctrl) {
 +                string += " + ctrl";
 +            }
 +            if (shift) {
 +                string += " + shift";
 +            }
 +            return string;
 +        }
 +    }
 +
 +    boolean collapseRequest;
 +    private boolean selectionPending;
 +    int colIndexOfHierarchy;
 +    String collapsedRowKey;
 +    VTreeTableScrollBody scrollBody;
 +    boolean animationsEnabled;
 +    LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTable.PendingNavigationEvent>();
 +    boolean focusParentResponsePending;
 +
 +    @Override
 +    protected VScrollTableBody createScrollBody() {
 +        scrollBody = new VTreeTableScrollBody();
 +        return scrollBody;
 +    }
 +
 +    /*
 +     * Overridden to allow animation of expands and collapses of nodes.
 +     */
 +    @Override
 +    protected void addAndRemoveRows(UIDL partialRowAdditions) {
 +        if (partialRowAdditions == null) {
 +            return;
 +        }
 +
 +        if (animationsEnabled) {
 +            if (partialRowAdditions.hasAttribute("hide")) {
 +                scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished(
 +                        partialRowAdditions.getIntAttribute("firstprowix"),
 +                        partialRowAdditions.getIntAttribute("numprows"));
 +            } else {
 +                scrollBody.insertRowsAnimated(partialRowAdditions,
 +                        partialRowAdditions.getIntAttribute("firstprowix"),
 +                        partialRowAdditions.getIntAttribute("numprows"));
 +                discardRowsOutsideCacheWindow();
 +            }
 +        } else {
 +            super.addAndRemoveRows(partialRowAdditions);
 +        }
 +    }
 +
 +    class VTreeTableScrollBody extends VScrollTable.VScrollTableBody {
 +        private int indentWidth = -1;
 +
 +        VTreeTableScrollBody() {
 +            super();
 +        }
 +
 +        @Override
 +        protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
 +            if (uidl.hasAttribute("gen_html")) {
 +                // This is a generated row.
 +                return new VTreeTableGeneratedRow(uidl, aligns2);
 +            }
 +            return new VTreeTableRow(uidl, aligns2);
 +        }
 +
 +        class VTreeTableRow extends
 +                VScrollTable.VScrollTableBody.VScrollTableRow {
 +
 +            private boolean isTreeCellAdded = false;
 +            private SpanElement treeSpacer;
 +            private boolean open;
 +            private int depth;
 +            private boolean canHaveChildren;
 +            protected Widget widgetInHierarchyColumn;
 +
 +            public VTreeTableRow(UIDL uidl, char[] aligns2) {
 +                super(uidl, aligns2);
 +            }
 +
 +            @Override
 +            public void addCell(UIDL rowUidl, String text, char align,
 +                    String style, boolean textIsHTML, boolean isSorted,
 +                    String description) {
 +                super.addCell(rowUidl, text, align, style, textIsHTML,
 +                        isSorted, description);
 +
 +                addTreeSpacer(rowUidl);
 +            }
 +
 +            protected boolean addTreeSpacer(UIDL rowUidl) {
 +                if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
 +                    Element container = (Element) getElement().getLastChild()
 +                            .getFirstChild();
 +
 +                    if (rowUidl.hasAttribute("icon")) {
 +                        // icons are in first content cell in TreeTable
 +                        ImageElement icon = Document.get().createImageElement();
 +                        icon.setClassName("v-icon");
 +                        icon.setAlt("icon");
 +                        icon.setSrc(client.translateVaadinUri(rowUidl
 +                                .getStringAttribute("icon")));
 +                        container.insertFirst(icon);
 +                    }
 +
 +                    String classname = "v-treetable-treespacer";
 +                    if (rowUidl.getBooleanAttribute("ca")) {
 +                        canHaveChildren = true;
 +                        open = rowUidl.getBooleanAttribute("open");
 +                        classname += open ? " v-treetable-node-open"
 +                                : " v-treetable-node-closed";
 +                    }
 +
 +                    treeSpacer = Document.get().createSpanElement();
 +
 +                    treeSpacer.setClassName(classname);
 +                    container.insertFirst(treeSpacer);
 +                    depth = rowUidl.hasAttribute("depth") ? rowUidl
 +                            .getIntAttribute("depth") : 0;
 +                    setIndent();
 +                    isTreeCellAdded = true;
 +                    return true;
 +                }
 +                return false;
 +            }
 +
 +            private boolean cellShowsTreeHierarchy(int curColIndex) {
 +                if (isTreeCellAdded) {
 +                    return false;
 +                }
-                 if (cellIx == colIndexOfHierarchy + (showRowHeaders ? 1 : 0)) {
++                return curColIndex == getHierarchyColumnIndex();
 +            }
 +
 +            @Override
 +            public void onBrowserEvent(Event event) {
 +                if (event.getEventTarget().cast() == treeSpacer
 +                        && treeSpacer.getClassName().contains("node")) {
 +                    if (event.getTypeInt() == Event.ONMOUSEUP) {
 +                        sendToggleCollapsedUpdate(getKey());
 +                    }
 +                    return;
 +                }
 +                super.onBrowserEvent(event);
 +            }
 +
 +            @Override
 +            public void addCell(UIDL rowUidl, Widget w, char align,
 +                    String style, boolean isSorted) {
 +                super.addCell(rowUidl, w, align, style, isSorted);
 +                if (addTreeSpacer(rowUidl)) {
 +                    widgetInHierarchyColumn = w;
 +                }
 +
 +            }
 +
 +            private void setIndent() {
 +                if (getIndentWidth() > 0) {
 +                    treeSpacer.getParentElement().getStyle()
 +                            .setPaddingLeft(getIndent(), Unit.PX);
 +                    treeSpacer.getStyle().setWidth(getIndent(), Unit.PX);
 +                }
 +            }
 +
 +            @Override
 +            protected void onAttach() {
 +                super.onAttach();
 +                if (getIndentWidth() < 0) {
 +                    detectIndent(this);
++                    // If we detect indent here then the size of the hierarchy
++                    // column is still wrong as it has been set when the indent
++                    // was not known.
++                    int w = getCellWidthFromDom(getHierarchyColumnIndex());
++                    if (w >= 0) {
++                        setColWidth(getHierarchyColumnIndex(), w);
++                    }
++                }
++            }
++
++            private int getCellWidthFromDom(int cellIndex) {
++                final Element cell = DOM.getChild(getElement(), cellIndex);
++                String w = cell.getStyle().getProperty("width");
++                if (w == null || "".equals(w) || !w.endsWith("px")) {
++                    return -1;
++                } else {
++                    return Integer.parseInt(w.substring(0, w.length() - 2));
 +                }
 +            }
 +
 +            private int getHierarchyAndIconWidth() {
 +                int consumedSpace = treeSpacer.getOffsetWidth();
 +                if (treeSpacer.getParentElement().getChildCount() > 2) {
 +                    // icon next to tree spacer
 +                    consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer
 +                            .getNextSibling()).getOffsetWidth();
 +                }
 +                return consumedSpace;
 +            }
 +
 +            @Override
 +            protected void setCellWidth(int cellIx, int width) {
-                     width = Math.max(width - getIndent(), 0);
++                if (cellIx == getHierarchyColumnIndex()) {
 +                    // take indentation padding into account if this is the
 +                    // hierarchy column
++                    int indent = getIndent();
++                    if (indent != -1) {
++                        width = Math.max(width - getIndent(), 0);
++                    }
 +                }
 +                super.setCellWidth(cellIx, width);
 +            }
 +
++            private int getHierarchyColumnIndex() {
++                return colIndexOfHierarchy + (showRowHeaders ? 1 : 0);
++            }
++
 +            private int getIndent() {
 +                return (depth + 1) * getIndentWidth();
 +            }
 +        }
 +
 +        protected class VTreeTableGeneratedRow extends VTreeTableRow {
 +            private boolean spanColumns;
 +            private boolean htmlContentAllowed;
 +
 +            public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) {
 +                super(uidl, aligns);
 +                addStyleName("v-table-generated-row");
 +            }
 +
 +            public boolean isSpanColumns() {
 +                return spanColumns;
 +            }
 +
 +            @Override
 +            protected void initCellWidths() {
 +                if (spanColumns) {
 +                    setSpannedColumnWidthAfterDOMFullyInited();
 +                } else {
 +                    super.initCellWidths();
 +                }
 +            }
 +
 +            private void setSpannedColumnWidthAfterDOMFullyInited() {
 +                // Defer setting width on spanned columns to make sure that
 +                // they are added to the DOM before trying to calculate
 +                // widths.
 +                Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 +
 +                    @Override
 +                    public void execute() {
 +                        if (showRowHeaders) {
 +                            setCellWidth(0, tHead.getHeaderCell(0).getWidth());
 +                            calcAndSetSpanWidthOnCell(1);
 +                        } else {
 +                            calcAndSetSpanWidthOnCell(0);
 +                        }
 +                    }
 +                });
 +            }
 +
 +            @Override
 +            protected boolean isRenderHtmlInCells() {
 +                return htmlContentAllowed;
 +            }
 +
 +            @Override
 +            protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
 +                    int visibleColumnIndex) {
 +                htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
 +                spanColumns = uidl.getBooleanAttribute("gen_span");
 +
 +                final Iterator<?> cells = uidl.getChildIterator();
 +                if (spanColumns) {
 +                    int colCount = uidl.getChildCount();
 +                    if (cells.hasNext()) {
 +                        final Object cell = cells.next();
 +                        if (cell instanceof String) {
 +                            addSpannedCell(uidl, cell.toString(), aligns[0],
 +                                    "", htmlContentAllowed, false, null,
 +                                    colCount);
 +                        } else {
 +                            addSpannedCell(uidl, (Widget) cell, aligns[0], "",
 +                                    false, colCount);
 +                        }
 +                    }
 +                } else {
 +                    super.addCellsFromUIDL(uidl, aligns, col,
 +                            visibleColumnIndex);
 +                }
 +            }
 +
 +            private void addSpannedCell(UIDL rowUidl, Widget w, char align,
 +                    String style, boolean sorted, int colCount) {
 +                TableCellElement td = DOM.createTD().cast();
 +                td.setColSpan(colCount);
 +                initCellWithWidget(w, align, style, sorted, td);
 +                td.getStyle().setHeight(getRowHeight(), Unit.PX);
 +                if (addTreeSpacer(rowUidl)) {
 +                    widgetInHierarchyColumn = w;
 +                }
 +            }
 +
 +            private void addSpannedCell(UIDL rowUidl, String text, char align,
 +                    String style, boolean textIsHTML, boolean sorted,
 +                    String description, int colCount) {
 +                // String only content is optimized by not using Label widget
 +                final TableCellElement td = DOM.createTD().cast();
 +                td.setColSpan(colCount);
 +                initCellWithText(text, align, style, textIsHTML, sorted,
 +                        description, td);
 +                td.getStyle().setHeight(getRowHeight(), Unit.PX);
 +                addTreeSpacer(rowUidl);
 +            }
 +
 +            @Override
 +            protected void setCellWidth(int cellIx, int width) {
 +                if (isSpanColumns()) {
 +                    if (showRowHeaders) {
 +                        if (cellIx == 0) {
 +                            super.setCellWidth(0, width);
 +                        } else {
 +                            // We need to recalculate the spanning TDs width for
 +                            // every cellIx in order to support column resizing.
 +                            calcAndSetSpanWidthOnCell(1);
 +                        }
 +                    } else {
 +                        // Same as above.
 +                        calcAndSetSpanWidthOnCell(0);
 +                    }
 +                } else {
 +                    super.setCellWidth(cellIx, width);
 +                }
 +            }
 +
 +            private void calcAndSetSpanWidthOnCell(final int cellIx) {
 +                int spanWidth = 0;
 +                for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
 +                        .getVisibleCellCount(); ix++) {
 +                    spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
 +                }
 +                Util.setWidthExcludingPaddingAndBorder((Element) getElement()
 +                        .getChild(cellIx), spanWidth, 13, false);
 +            }
 +        }
 +
 +        private int getIndentWidth() {
 +            return indentWidth;
 +        }
 +
 +        private void detectIndent(VTreeTableRow vTreeTableRow) {
 +            indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
 +            if (indentWidth == 0) {
 +                indentWidth = -1;
 +                return;
 +            }
 +            Iterator<Widget> iterator = iterator();
 +            while (iterator.hasNext()) {
 +                VTreeTableRow next = (VTreeTableRow) iterator.next();
 +                next.setIndent();
 +            }
 +        }
 +
 +        protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished(
 +                final int firstIndex, final int rows) {
 +            List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>();
 +            for (int ix = firstIndex; ix < firstIndex + rows; ix++) {
 +                VScrollTableRow row = getRowByRowIndex(ix);
 +                if (row != null) {
 +                    rowsToDelete.add(row);
 +                }
 +            }
 +            if (!rowsToDelete.isEmpty()) {
 +                // #8810 Only animate if there's something to animate
 +                RowCollapseAnimation anim = new RowCollapseAnimation(
 +                        rowsToDelete) {
 +                    @Override
 +                    protected void onComplete() {
 +                        super.onComplete();
 +                        // Actually unlink the rows and update the cache after
 +                        // the
 +                        // animation is done.
 +                        unlinkAndReindexRows(firstIndex, rows);
 +                        discardRowsOutsideCacheWindow();
 +                        ensureCacheFilled();
 +                    }
 +                };
 +                anim.run(150);
 +            }
 +        }
 +
 +        protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData,
 +                int firstIndex, int rows) {
 +            List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData,
 +                    firstIndex, rows);
 +            if (!insertedRows.isEmpty()) {
 +                // Only animate if there's something to animate (#8810)
 +                RowExpandAnimation anim = new RowExpandAnimation(insertedRows);
 +                anim.run(150);
 +            }
 +            return insertedRows;
 +        }
 +
 +        /**
 +         * Prepares the table for animation by copying the background colors of
 +         * all TR elements to their respective TD elements if the TD element is
 +         * transparent. This is needed, since if TDs have transparent
 +         * backgrounds, the rows sliding behind them are visible.
 +         */
 +        private class AnimationPreparator {
 +            private final int lastItemIx;
 +
 +            public AnimationPreparator(int lastItemIx) {
 +                this.lastItemIx = lastItemIx;
 +            }
 +
 +            public void prepareTableForAnimation() {
 +                int ix = lastItemIx;
 +                VScrollTableRow row = null;
 +                while ((row = getRowByRowIndex(ix)) != null) {
 +                    copyTRBackgroundsToTDs(row);
 +                    --ix;
 +                }
 +            }
 +
 +            private void copyTRBackgroundsToTDs(VScrollTableRow row) {
 +                Element tr = row.getElement();
 +                ComputedStyle cs = new ComputedStyle(tr);
 +                String backgroundAttachment = cs
 +                        .getProperty("backgroundAttachment");
 +                String backgroundClip = cs.getProperty("backgroundClip");
 +                String backgroundColor = cs.getProperty("backgroundColor");
 +                String backgroundImage = cs.getProperty("backgroundImage");
 +                String backgroundOrigin = cs.getProperty("backgroundOrigin");
 +                for (int ix = 0; ix < tr.getChildCount(); ix++) {
 +                    Element td = tr.getChild(ix).cast();
 +                    if (!elementHasBackground(td)) {
 +                        td.getStyle().setProperty("backgroundAttachment",
 +                                backgroundAttachment);
 +                        td.getStyle().setProperty("backgroundClip",
 +                                backgroundClip);
 +                        td.getStyle().setProperty("backgroundColor",
 +                                backgroundColor);
 +                        td.getStyle().setProperty("backgroundImage",
 +                                backgroundImage);
 +                        td.getStyle().setProperty("backgroundOrigin",
 +                                backgroundOrigin);
 +                    }
 +                }
 +            }
 +
 +            private boolean elementHasBackground(Element element) {
 +                ComputedStyle cs = new ComputedStyle(element);
 +                String clr = cs.getProperty("backgroundColor");
 +                String img = cs.getProperty("backgroundImage");
 +                return !("rgba(0, 0, 0, 0)".equals(clr.trim())
 +                        || "transparent".equals(clr.trim()) || img == null);
 +            }
 +
 +            public void restoreTableAfterAnimation() {
 +                int ix = lastItemIx;
 +                VScrollTableRow row = null;
 +                while ((row = getRowByRowIndex(ix)) != null) {
 +                    restoreStyleForTDsInRow(row);
 +
 +                    --ix;
 +                }
 +            }
 +
 +            private void restoreStyleForTDsInRow(VScrollTableRow row) {
 +                Element tr = row.getElement();
 +                for (int ix = 0; ix < tr.getChildCount(); ix++) {
 +                    Element td = tr.getChild(ix).cast();
 +                    td.getStyle().clearProperty("backgroundAttachment");
 +                    td.getStyle().clearProperty("backgroundClip");
 +                    td.getStyle().clearProperty("backgroundColor");
 +                    td.getStyle().clearProperty("backgroundImage");
 +                    td.getStyle().clearProperty("backgroundOrigin");
 +                }
 +            }
 +        }
 +
 +        /**
 +         * Animates row expansion using the GWT animation framework.
 +         * 
 +         * The idea is as follows:
 +         * 
 +         * 1. Insert all rows normally
 +         * 
 +         * 2. Insert a newly created DIV containing a new TABLE element below
 +         * the DIV containing the actual scroll table body.
 +         * 
 +         * 3. Clone the rows that were inserted in step 1 and attach the clones
 +         * to the new TABLE element created in step 2.
 +         * 
 +         * 4. The new DIV from step 2 is absolutely positioned so that the last
 +         * inserted row is just behind the row that was expanded.
 +         * 
 +         * 5. Hide the contents of the originally inserted rows by setting the
 +         * DIV.v-table-cell-wrapper to display:none;.
 +         * 
 +         * 6. Set the height of the originally inserted rows to 0.
 +         * 
 +         * 7. The animation loop slides the DIV from step 2 downwards, while at
 +         * the same pace growing the height of each of the inserted rows from 0
 +         * to full height. The first inserted row grows from 0 to full and after
 +         * this the second row grows from 0 to full, etc until all rows are full
 +         * height.
 +         * 
 +         * 8. Remove the DIV from step 2
 +         * 
 +         * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements.
 +         * 
 +         * 10. DONE
 +         */
 +        private class RowExpandAnimation extends Animation {
 +
 +            private final List<VScrollTableRow> rows;
 +            private Element cloneDiv;
 +            private Element cloneTable;
 +            private AnimationPreparator preparator;
 +
 +            /**
 +             * @param rows
 +             *            List of rows to animate. Must not be empty.
 +             */
 +            public RowExpandAnimation(List<VScrollTableRow> rows) {
 +                this.rows = rows;
 +                buildAndInsertAnimatingDiv();
 +                preparator = new AnimationPreparator(rows.get(0).getIndex() - 1);
 +                preparator.prepareTableForAnimation();
 +                for (VScrollTableRow row : rows) {
 +                    cloneAndAppendRow(row);
 +                    row.addStyleName("v-table-row-animating");
 +                    setCellWrapperDivsToDisplayNone(row);
 +                    row.setHeight(getInitialHeight());
 +                }
 +            }
 +
 +            protected String getInitialHeight() {
 +                return "0px";
 +            }
 +
 +            private void cloneAndAppendRow(VScrollTableRow row) {
 +                Element clonedTR = null;
 +                clonedTR = row.getElement().cloneNode(true).cast();
 +                clonedTR.getStyle().setVisibility(Visibility.VISIBLE);
 +                cloneTable.appendChild(clonedTR);
 +            }
 +
 +            protected double getBaseOffset() {
 +                return rows.get(0).getAbsoluteTop()
 +                        - rows.get(0).getParent().getAbsoluteTop()
 +                        - rows.size() * getRowHeight();
 +            }
 +
 +            private void buildAndInsertAnimatingDiv() {
 +                cloneDiv = DOM.createDiv();
 +                cloneDiv.addClassName("v-treetable-animation-clone-wrapper");
 +                cloneTable = DOM.createTable();
 +                cloneTable.addClassName("v-treetable-animation-clone");
 +                cloneDiv.appendChild(cloneTable);
 +                insertAnimatingDiv();
 +            }
 +
 +            private void insertAnimatingDiv() {
 +                Element tableBody = getElement().cast();
 +                Element tableBodyParent = tableBody.getParentElement().cast();
 +                tableBodyParent.insertAfter(cloneDiv, tableBody);
 +            }
 +
 +            @Override
 +            protected void onUpdate(double progress) {
 +                animateDiv(progress);
 +                animateRowHeights(progress);
 +            }
 +
 +            private void animateDiv(double progress) {
 +                double offset = calculateDivOffset(progress, getRowHeight());
 +
 +                cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX);
 +            }
 +
 +            private void animateRowHeights(double progress) {
 +                double rh = getRowHeight();
 +                double vlh = calculateHeightOfAllVisibleLines(progress, rh);
 +                int ix = 0;
 +
 +                while (ix < rows.size()) {
 +                    double height = vlh < rh ? vlh : rh;
 +                    rows.get(ix).setHeight(height + "px");
 +                    vlh -= height;
 +                    ix++;
 +                }
 +            }
 +
 +            protected double calculateHeightOfAllVisibleLines(double progress,
 +                    double rh) {
 +                return rows.size() * rh * progress;
 +            }
 +
 +            protected double calculateDivOffset(double progress, double rh) {
 +                return progress * rows.size() * rh;
 +            }
 +
 +            @Override
 +            protected void onComplete() {
 +                preparator.restoreTableAfterAnimation();
 +                for (VScrollTableRow row : rows) {
 +                    resetCellWrapperDivsDisplayProperty(row);
 +                    row.removeStyleName("v-table-row-animating");
 +                }
 +                Element tableBodyParent = (Element) getElement()
 +                        .getParentElement();
 +                tableBodyParent.removeChild(cloneDiv);
 +            }
 +
 +            private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) {
 +                Element tr = row.getElement();
 +                for (int ix = 0; ix < tr.getChildCount(); ix++) {
 +                    getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE);
 +                }
 +            }
 +
 +            private Element getWrapperDiv(Element tr, int tdIx) {
 +                Element td = tr.getChild(tdIx).cast();
 +                return td.getChild(0).cast();
 +            }
 +
 +            private void resetCellWrapperDivsDisplayProperty(VScrollTableRow row) {
 +                Element tr = row.getElement();
 +                for (int ix = 0; ix < tr.getChildCount(); ix++) {
 +                    getWrapperDiv(tr, ix).getStyle().clearProperty("display");
 +                }
 +            }
 +
 +        }
 +
 +        /**
 +         * This is the inverse of the RowExpandAnimation and is implemented by
 +         * extending it and overriding the calculation of offsets and heights.
 +         */
 +        private class RowCollapseAnimation extends RowExpandAnimation {
 +
 +            private final List<VScrollTableRow> rows;
 +
 +            /**
 +             * @param rows
 +             *            List of rows to animate. Must not be empty.
 +             */
 +            public RowCollapseAnimation(List<VScrollTableRow> rows) {
 +                super(rows);
 +                this.rows = rows;
 +            }
 +
 +            @Override
 +            protected String getInitialHeight() {
 +                return getRowHeight() + "px";
 +            }
 +
 +            @Override
 +            protected double getBaseOffset() {
 +                return getRowHeight();
 +            }
 +
 +            @Override
 +            protected double calculateHeightOfAllVisibleLines(double progress,
 +                    double rh) {
 +                return rows.size() * rh * (1 - progress);
 +            }
 +
 +            @Override
 +            protected double calculateDivOffset(double progress, double rh) {
 +                return -super.calculateDivOffset(progress, rh);
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Icons rendered into first actual column in TreeTable, not to row header
 +     * cell
 +     */
 +    @Override
 +    protected String buildCaptionHtmlSnippet(UIDL uidl) {
 +        if (uidl.getTag().equals("column")) {
 +            return super.buildCaptionHtmlSnippet(uidl);
 +        } else {
 +            String s = uidl.getStringAttribute("caption");
 +            return s;
 +        }
 +    }
 +
 +    @Override
 +    protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
 +        if (collapseRequest || focusParentResponsePending) {
 +            // Enqueue the event if there might be pending content changes from
 +            // the server
 +            if (pendingNavigationEvents.size() < 10) {
 +                // Only keep 10 keyboard events in the queue
 +                PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent(
 +                        keycode, ctrl, shift);
 +                pendingNavigationEvents.add(pendingNavigationEvent);
 +            }
 +            return true;
 +        }
 +
 +        VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow();
 +        if (focusedRow != null) {
 +            if (focusedRow.canHaveChildren
 +                    && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) {
 +                if (!ctrl) {
 +                    client.updateVariable(paintableId, "selectCollapsed", true,
 +                            false);
 +                }
 +                sendSelectedRows(false);
 +                sendToggleCollapsedUpdate(focusedRow.getKey());
 +                return true;
 +            } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) {
 +                // already expanded, move selection down if next is on a deeper
 +                // level (is-a-child)
 +                VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow
 +                        .getParent();
 +                Iterator<Widget> iterator = body.iterator();
 +                VTreeTableRow next = null;
 +                while (iterator.hasNext()) {
 +                    next = (VTreeTableRow) iterator.next();
 +                    if (next == focusedRow) {
 +                        next = (VTreeTableRow) iterator.next();
 +                        break;
 +                    }
 +                }
 +                if (next != null) {
 +                    if (next.depth > focusedRow.depth) {
 +                        selectionPending = true;
 +                        return super.handleNavigation(getNavigationDownKey(),
 +                                ctrl, shift);
 +                    }
 +                } else {
 +                    // Note, a minor change here for a bit false behavior if
 +                    // cache rows is disabled + last visible row + no childs for
 +                    // the node
 +                    selectionPending = true;
 +                    return super.handleNavigation(getNavigationDownKey(), ctrl,
 +                            shift);
 +                }
 +            } else if (keycode == KeyCodes.KEY_LEFT) {
 +                // already collapsed move selection up to parent node
 +                // do on the server side as the parent is not necessary
 +                // rendered on the client, could check if parent is visible if
 +                // a performance issue arises
 +
 +                client.updateVariable(paintableId, "focusParent",
 +                        focusedRow.getKey(), true);
 +
 +                // Set flag that we should enqueue navigation events until we
 +                // get a response to this request
 +                focusParentResponsePending = true;
 +
 +                return true;
 +            }
 +        }
 +        return super.handleNavigation(keycode, ctrl, shift);
 +    }
 +
 +    private void sendToggleCollapsedUpdate(String rowKey) {
 +        collapsedRowKey = rowKey;
 +        collapseRequest = true;
 +        client.updateVariable(paintableId, "toggleCollapsed", rowKey, true);
 +    }
 +
 +    @Override
 +    public void onBrowserEvent(Event event) {
 +        super.onBrowserEvent(event);
 +        if (event.getTypeInt() == Event.ONKEYUP && selectionPending) {
 +            sendSelectedRows();
 +        }
 +    }
 +
 +    @Override
 +    protected void sendSelectedRows(boolean immediately) {
 +        super.sendSelectedRows(immediately);
 +        selectionPending = false;
 +    }
 +
 +    @Override
 +    protected void reOrderColumn(String columnKey, int newIndex) {
 +        super.reOrderColumn(columnKey, newIndex);
 +        // current impl not intelligent enough to survive without visiting the
 +        // server to redraw content
 +        client.sendPendingVariableChanges();
 +    }
 +
 +    @Override
 +    public void setStyleName(String style) {
 +        super.setStyleName(style + " v-treetable");
 +    }
 +
 +    @Override
 +    protected void updateTotalRows(UIDL uidl) {
 +        // Make sure that initializedAndAttached & al are not reset when the
 +        // totalrows are updated on expand/collapse requests.
 +        int newTotalRows = uidl.getIntAttribute("totalrows");
 +        setTotalRows(newTotalRows);
 +    }
 +
 +}
index a1bab916187aff35330f52101b30499648b7c93f,0000000000000000000000000000000000000000..3ee266b9440b18bd61767633ae8d490ba1661baa
mode 100644,000000..100644
--- /dev/null
@@@ -1,318 -1,0 +1,318 @@@
-         implements Paintable, BeforeShortcutActionListener,
-         SimpleManagedLayout, PostLayoutListener, MayScrollChildren {
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +package com.vaadin.terminal.gwt.client.ui.window;
 +
 +import com.google.gwt.dom.client.Element;
 +import com.google.gwt.dom.client.NativeEvent;
 +import com.google.gwt.dom.client.Style;
 +import com.google.gwt.dom.client.Style.Position;
 +import com.google.gwt.dom.client.Style.Unit;
 +import com.google.gwt.user.client.DOM;
 +import com.google.gwt.user.client.Event;
 +import com.google.gwt.user.client.Window;
 +import com.google.gwt.user.client.ui.Widget;
 +import com.vaadin.shared.MouseEventDetails;
 +import com.vaadin.shared.ui.Connect;
 +import com.vaadin.shared.ui.window.WindowServerRpc;
 +import com.vaadin.shared.ui.window.WindowState;
 +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.ConnectorHierarchyChangeEvent;
 +import com.vaadin.terminal.gwt.client.LayoutManager;
 +import com.vaadin.terminal.gwt.client.Paintable;
 +import com.vaadin.terminal.gwt.client.UIDL;
 +import com.vaadin.terminal.gwt.client.communication.RpcProxy;
 +import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
 +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
 +import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
 +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
 +import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener;
 +import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
 +import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
 +
 +@Connect(value = com.vaadin.ui.Window.class)
 +public class WindowConnector extends AbstractComponentContainerConnector
-                                                // possible centering
++implements Paintable, BeforeShortcutActionListener,
++SimpleManagedLayout, PostLayoutListener, MayScrollChildren {
 +
 +    private ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
 +        @Override
 +        protected void fireClick(NativeEvent event,
 +                MouseEventDetails mouseDetails) {
 +            rpc.click(mouseDetails);
 +        }
 +    };
 +
 +    private WindowServerRpc rpc;
 +
 +    boolean minWidthChecked = false;
 +
 +    @Override
 +    public boolean delegateCaptionHandling() {
 +        return false;
 +    };
 +
 +    @Override
 +    protected void init() {
 +        super.init();
 +        rpc = RpcProxy.create(WindowServerRpc.class, this);
 +
 +        getLayoutManager().registerDependency(this,
 +                getWidget().contentPanel.getElement());
 +        getLayoutManager().registerDependency(this, getWidget().header);
 +        getLayoutManager().registerDependency(this, getWidget().footer);
 +    }
 +
 +    @Override
 +    public void onUnregister() {
 +        LayoutManager lm = getLayoutManager();
 +        VWindow window = getWidget();
 +        lm.unregisterDependency(this, window.contentPanel.getElement());
 +        lm.unregisterDependency(this, window.header);
 +        lm.unregisterDependency(this, window.footer);
 +    }
 +
 +    @Override
 +    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
 +        getWidget().id = getConnectorId();
 +        getWidget().client = client;
 +
 +        // Workaround needed for Testing Tools (GWT generates window DOM
 +        // slightly different in different browsers).
 +        DOM.setElementProperty(getWidget().closeBox, "id", getConnectorId()
 +                + "_window_close");
 +
 +        if (isRealUpdate(uidl)) {
 +            if (getState().isModal() != getWidget().vaadinModality) {
 +                getWidget().setVaadinModality(!getWidget().vaadinModality);
 +            }
 +            if (!getWidget().isAttached()) {
 +                getWidget().setVisible(false); // hide until
-         window.sizeOrPositionUpdated();
++                // possible centering
 +                getWidget().show();
 +            }
 +            if (getState().isResizable() != getWidget().resizable) {
 +                getWidget().setResizable(getState().isResizable());
 +            }
 +            getWidget().resizeLazy = getState().isResizeLazy();
 +
 +            getWidget().setDraggable(getState().isDraggable());
 +
 +            // Caption must be set before required header size is measured. If
 +            // the caption attribute is missing the caption should be cleared.
 +            String iconURL = null;
 +            if (getState().getIcon() != null) {
 +                iconURL = getState().getIcon().getURL();
 +            }
 +            getWidget().setCaption(getState().getCaption(), iconURL);
 +        }
 +
 +        getWidget().visibilityChangesDisabled = true;
 +        if (!isRealUpdate(uidl)) {
 +            return;
 +        }
 +        getWidget().visibilityChangesDisabled = false;
 +
 +        clickEventHandler.handleEventHandlerRegistration();
 +
 +        getWidget().immediate = getState().isImmediate();
 +
 +        getWidget().setClosable(!isReadOnly());
 +
 +        // Initialize the position form UIDL
 +        int positionx = getState().getPositionX();
 +        int positiony = getState().getPositionY();
 +        if (positionx >= 0 || positiony >= 0) {
 +            if (positionx < 0) {
 +                positionx = 0;
 +            }
 +            if (positiony < 0) {
 +                positiony = 0;
 +            }
 +            getWidget().setPopupPosition(positionx, positiony);
 +        }
 +
 +        int childIndex = 0;
 +
 +        // we may have actions
 +        for (int i = 0; i < uidl.getChildCount(); i++) {
 +            UIDL childUidl = uidl.getChildUIDL(i);
 +            if (childUidl.getTag().equals("actions")) {
 +                if (getWidget().shortcutHandler == null) {
 +                    getWidget().shortcutHandler = new ShortcutActionHandler(
 +                            getConnectorId(), client);
 +                }
 +                getWidget().shortcutHandler.updateActionMap(childUidl);
 +            }
 +
 +        }
 +
 +        // setting scrollposition must happen after children is rendered
 +        getWidget().contentPanel.setScrollPosition(getState().getScrollTop());
 +        getWidget().contentPanel.setHorizontalScrollPosition(getState()
 +                .getScrollLeft());
 +
 +        // Center this window on screen if requested
 +        // This had to be here because we might not know the content size before
 +        // everything is painted into the window
 +
 +        // centered is this is unset on move/resize
 +        getWidget().centered = getState().isCentered();
 +        getWidget().setVisible(true);
 +
 +        // ensure window is not larger than browser window
 +        if (getWidget().getOffsetWidth() > Window.getClientWidth()) {
 +            getWidget().setWidth(Window.getClientWidth() + "px");
 +        }
 +        if (getWidget().getOffsetHeight() > Window.getClientHeight()) {
 +            getWidget().setHeight(Window.getClientHeight() + "px");
 +        }
 +
 +        if (uidl.hasAttribute("bringToFront")) {
 +            /*
 +             * Focus as a side-effect. Will be overridden by
 +             * ApplicationConnection if another component was focused by the
 +             * server side.
 +             */
 +            getWidget().contentPanel.focus();
 +            getWidget().bringToFrontSequence = uidl
 +                    .getIntAttribute("bringToFront");
 +            VWindow.deferOrdering();
 +        }
 +    }
 +
 +    @Override
 +    public void updateCaption(ComponentConnector component) {
 +        // NOP, window has own caption, layout caption not rendered
 +    }
 +
 +    @Override
 +    public void onBeforeShortcutAction(Event e) {
 +        // NOP, nothing to update just avoid workaround ( causes excess
 +        // blur/focus )
 +    }
 +
 +    @Override
 +    public VWindow getWidget() {
 +        return (VWindow) super.getWidget();
 +    }
 +
 +    @Override
 +    public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
 +        super.onConnectorHierarchyChange(event);
 +
 +        // We always have 1 child, unless the child is hidden
 +        Widget newChildWidget = null;
 +        ComponentConnector newChild = null;
 +        if (getChildComponents().size() == 1) {
 +            newChild = getChildComponents().get(0);
 +            newChildWidget = newChild.getWidget();
 +        }
 +
 +        getWidget().layout = newChild;
 +        getWidget().contentPanel.setWidget(newChildWidget);
 +    }
 +
 +    @Override
 +    public void layout() {
 +        LayoutManager lm = getLayoutManager();
 +        VWindow window = getWidget();
 +        ComponentConnector layout = window.layout;
 +        Element contentElement = window.contentPanel.getElement();
 +
 +        if (!minWidthChecked) {
 +            boolean needsMinWidth = !isUndefinedWidth()
 +                    || layout.isRelativeWidth();
 +            int minWidth = window.getMinWidth();
 +            if (needsMinWidth && lm.getInnerWidth(contentElement) < minWidth) {
 +                minWidthChecked = true;
 +                // Use minimum width if less than a certain size
 +                window.setWidth(minWidth + "px");
 +            }
 +            minWidthChecked = true;
 +        }
 +
 +        boolean needsMinHeight = !isUndefinedHeight()
 +                || layout.isRelativeHeight();
 +        int minHeight = window.getMinHeight();
 +        if (needsMinHeight && lm.getInnerHeight(contentElement) < minHeight) {
 +            // Use minimum height if less than a certain size
 +            window.setHeight(minHeight + "px");
 +        }
 +
 +        Style contentStyle = window.contents.getStyle();
 +
 +        int headerHeight = lm.getOuterHeight(window.header);
 +        contentStyle.setPaddingTop(headerHeight, Unit.PX);
 +        contentStyle.setMarginTop(-headerHeight, Unit.PX);
 +
 +        int footerHeight = lm.getOuterHeight(window.footer);
 +        contentStyle.setPaddingBottom(footerHeight, Unit.PX);
 +        contentStyle.setMarginBottom(-footerHeight, Unit.PX);
 +
 +        /*
 +         * Must set absolute position if the child has relative height and
 +         * there's a chance of horizontal scrolling as some browsers will
 +         * otherwise not take the scrollbar into account when calculating the
 +         * height.
 +         */
 +        Element layoutElement = layout.getWidget().getElement();
 +        Style childStyle = layoutElement.getStyle();
 +        if (layout.isRelativeHeight() && !BrowserInfo.get().isIE9()) {
 +            childStyle.setPosition(Position.ABSOLUTE);
 +
 +            Style wrapperStyle = contentElement.getStyle();
 +            if (window.getElement().getStyle().getWidth().length() == 0
 +                    && !layout.isRelativeWidth()) {
 +                /*
 +                 * Need to lock width to make undefined width work even with
 +                 * absolute positioning
 +                 */
 +                int contentWidth = lm.getOuterWidth(layoutElement);
 +                wrapperStyle.setWidth(contentWidth, Unit.PX);
 +            } else {
 +                wrapperStyle.clearWidth();
 +            }
 +        } else {
 +            childStyle.clearPosition();
 +        }
 +    }
 +
 +    @Override
 +    public void postLayout() {
 +        minWidthChecked = false;
 +        VWindow window = getWidget();
 +        if (window.centered) {
 +            window.center();
 +        }
++        window.positionOrSizeUpdated();
 +    }
 +
 +    @Override
 +    public WindowState getState() {
 +        return (WindowState) super.getState();
 +    }
 +
 +    /**
 +     * Gives the WindowConnector an order number. As a side effect, moves the
 +     * window according to its order number so the windows are stacked. This
 +     * method should be called for each window in the order they should appear.
 +     */
 +    public void setWindowOrderAndPosition() {
 +        getWidget().setWindowOrderAndPosition();
 +    }
 +}
index 3e55633574a5f42d5d67f64f5fae665f0e3463c4,0000000000000000000000000000000000000000..0146c92b5c18c7279d088efbfc9f534d94aebd55
mode 100644,000000..100644
--- /dev/null
@@@ -1,260 -1,0 +1,363 @@@
-     public void setValue(Object newValue) throws ReadOnlyException {
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +package com.vaadin.data.util.sqlcontainer;
 +
 +import java.sql.Date;
 +import java.sql.Time;
 +import java.sql.Timestamp;
 +
 +import com.vaadin.data.Property;
++import com.vaadin.data.util.converter.Converter.ConversionException;
 +
 +/**
 + * ColumnProperty represents the value of one column in a RowItem. In addition
 + * to the value, ColumnProperty also contains some basic column attributes such
 + * as nullability status, read-only status and data type.
 + * 
 + * Note that depending on the QueryDelegate in use this does not necessarily map
 + * into an actual column in a database table.
 + */
 +final public class ColumnProperty implements Property {
 +    private static final long serialVersionUID = -3694463129581802457L;
 +
 +    private RowItem owner;
 +
 +    private String propertyId;
 +
 +    private boolean readOnly;
 +    private boolean allowReadOnlyChange = true;
 +    private boolean nullable = true;
 +
 +    private Object value;
 +    private Object changedValue;
 +    private Class<?> type;
 +
 +    private boolean modified;
 +
 +    private boolean versionColumn;
++    private boolean primaryKey = false;
 +
 +    /**
 +     * Prevent instantiation without required parameters.
 +     */
 +    @SuppressWarnings("unused")
 +    private ColumnProperty() {
 +    }
 +
++    /**
++     * Deprecated constructor for ColumnProperty. If this is used the primary
++     * keys are not identified correctly in some cases for some databases (i.e.
++     * Oracle). See http://dev.vaadin.com/ticket/9145.
++     * 
++     * @param propertyId
++     * @param readOnly
++     * @param allowReadOnlyChange
++     * @param nullable
++     * @param value
++     * @param type
++     * 
++     * @deprecated
++     */
++    @Deprecated
 +    public ColumnProperty(String propertyId, boolean readOnly,
 +            boolean allowReadOnlyChange, boolean nullable, Object value,
 +            Class<?> type) {
++        this(propertyId, readOnly, allowReadOnlyChange, nullable, false, value,
++                type);
++    }
++
++    /**
++     * Creates a new ColumnProperty instance.
++     * 
++     * @param propertyId
++     *            The ID of this property.
++     * @param readOnly
++     *            Whether this property is read-only.
++     * @param allowReadOnlyChange
++     *            Whether the read-only status of this property can be changed.
++     * @param nullable
++     *            Whether this property accepts null values.
++     * @param primaryKey
++     *            Whether this property corresponds to a database primary key.
++     * @param value
++     *            The value of this property.
++     * @param type
++     *            The type of this property.
++     */
++    public ColumnProperty(String propertyId, boolean readOnly,
++            boolean allowReadOnlyChange, boolean nullable, boolean primaryKey,
++            Object value, Class<?> type) {
++
 +        if (propertyId == null) {
 +            throw new IllegalArgumentException("Properties must be named.");
 +        }
 +        if (type == null) {
 +            throw new IllegalArgumentException("Property type must be set.");
 +        }
 +        this.propertyId = propertyId;
 +        this.type = type;
 +        this.value = value;
 +
 +        this.allowReadOnlyChange = allowReadOnlyChange;
 +        this.nullable = nullable;
 +        this.readOnly = readOnly;
++        this.primaryKey = primaryKey;
 +    }
 +
++    /**
++     * Returns the current value for this property. To get the previous value
++     * (if one exists) for a modified property use {@link #getOldValue()}.
++     * 
++     * @return
++     */
 +    @Override
 +    public Object getValue() {
 +        if (isModified()) {
 +            return changedValue;
 +        }
 +        return value;
 +    }
 +
++    /**
++     * Returns the original non-modified value of this property if it has been
++     * modified.
++     * 
++     * @return The original value if <code>isModified()</code> is true,
++     *         <code>getValue()</code> otherwise.
++     */
++    public Object getOldValue() {
++        return value;
++    }
++
 +    @Override
++    public void setValue(Object newValue) throws ReadOnlyException,
++    ConversionException {
 +        if (newValue == null && !nullable) {
 +            throw new NotNullableException(
 +                    "Null values are not allowed for this property.");
 +        }
 +        if (readOnly) {
 +            throw new ReadOnlyException(
 +                    "Cannot set value for read-only property.");
 +        }
 +
 +        /* Check if this property is a date property. */
 +        boolean isDateProperty = Time.class.equals(getType())
 +                || Date.class.equals(getType())
 +                || Timestamp.class.equals(getType());
 +
 +        if (newValue != null) {
 +            /* Handle SQL dates, times and Timestamps given as java.util.Date */
 +            if (isDateProperty) {
 +                /*
 +                 * Try to get the millisecond value from the new value of this
 +                 * property. Possible type to convert from is java.util.Date.
 +                 */
 +                long millis = 0;
 +                if (newValue instanceof java.util.Date) {
 +                    millis = ((java.util.Date) newValue).getTime();
 +                    /*
 +                     * Create the new object based on the millisecond value,
 +                     * according to the type of this property.
 +                     */
 +                    if (Time.class.equals(getType())) {
 +                        newValue = new Time(millis);
 +                    } else if (Date.class.equals(getType())) {
 +                        newValue = new Date(millis);
 +                    } else if (Timestamp.class.equals(getType())) {
 +                        newValue = new Timestamp(millis);
 +                    }
 +                }
 +            }
 +
 +            if (!getType().isAssignableFrom(newValue.getClass())) {
 +                throw new IllegalArgumentException(
 +                        "Illegal value type for ColumnProperty");
 +            }
 +
 +            /*
 +             * If the value to be set is the same that has already been set, do
 +             * not set it again.
 +             */
 +            if (isValueAlreadySet(newValue)) {
 +                return;
 +            }
 +        }
 +
 +        /* Set the new value and notify container of the change. */
 +        changedValue = newValue;
 +        modified = true;
 +        owner.getContainer().itemChangeNotification(owner);
 +    }
 +
 +    private boolean isValueAlreadySet(Object newValue) {
 +        Object referenceValue = isModified() ? changedValue : value;
 +
 +        return (isNullable() && newValue == null && referenceValue == null)
 +                || newValue.equals(referenceValue);
 +    }
 +
 +    @Override
 +    public Class<?> getType() {
 +        return type;
 +    }
 +
 +    @Override
 +    public boolean isReadOnly() {
 +        return readOnly;
 +    }
 +
++    /**
++     * Returns whether the read-only status of this property can be changed
++     * using {@link #setReadOnly(boolean)}.
++     * <p>
++     * Used to prevent setting to read/write mode a property that is not allowed
++     * to be written by the underlying database. Also used for values like
++     * VERSION and AUTO_INCREMENT fields that might be set to read-only by the
++     * container but the database still allows writes.
++     * 
++     * @return true if the read-only status can be changed, false otherwise.
++     */
 +    public boolean isReadOnlyChangeAllowed() {
 +        return allowReadOnlyChange;
 +    }
 +
 +    @Override
 +    public void setReadOnly(boolean newStatus) {
 +        if (allowReadOnlyChange) {
 +            readOnly = newStatus;
 +        }
 +    }
 +
++    public boolean isPrimaryKey() {
++        return primaryKey;
++    }
++
 +    public String getPropertyId() {
 +        return propertyId;
 +    }
 +
 +    /**
 +     * Returns the value of the Property in human readable textual format.
 +     * 
 +     * @see java.lang.Object#toString()
 +     * @deprecated get the string representation from the value
 +     */
 +    @Deprecated
 +    @Override
 +    public String toString() {
 +        throw new UnsupportedOperationException(
 +                "Use ColumnProperty.getValue() instead of ColumnProperty.toString()");
 +    }
 +
 +    public void setOwner(RowItem owner) {
 +        if (owner == null) {
 +            throw new IllegalArgumentException("Owner can not be set to null.");
 +        }
 +        if (this.owner != null) {
 +            throw new IllegalStateException(
 +                    "ColumnProperties can only be bound once.");
 +        }
 +        this.owner = owner;
 +    }
 +
 +    public boolean isModified() {
 +        return modified;
 +    }
 +
 +    public boolean isVersionColumn() {
 +        return versionColumn;
 +    }
 +
 +    public void setVersionColumn(boolean versionColumn) {
 +        this.versionColumn = versionColumn;
 +    }
 +
 +    public boolean isNullable() {
 +        return nullable;
 +    }
 +
++    /**
++     * Return whether the value of this property should be persisted to the
++     * database.
++     * 
++     * @return true if the value should be written to the database, false
++     *         otherwise.
++     */
++    public boolean isPersistent() {
++        if (isVersionColumn()) {
++            return false;
++        } else if (isReadOnlyChangeAllowed() && !isReadOnly()) {
++            return true;
++        } else {
++            return false;
++        }
++    }
++
++    /**
++     * Returns whether or not this property is used as a row identifier.
++     * 
++     * @return true if the property is a row identifier, false otherwise.
++     */
++    public boolean isRowIdentifier() {
++        return isPrimaryKey() || isVersionColumn();
++    }
++
 +    /**
 +     * An exception that signals that a <code>null</code> value was passed to
 +     * the <code>setValue</code> method, but the value of this property can not
 +     * be set to <code>null</code>.
 +     */
 +    @SuppressWarnings("serial")
 +    public class NotNullableException extends RuntimeException {
 +
 +        /**
 +         * Constructs a new <code>NotNullableException</code> without a detail
 +         * message.
 +         */
 +        public NotNullableException() {
 +        }
 +
 +        /**
 +         * Constructs a new <code>NotNullableException</code> with the specified
 +         * detail message.
 +         * 
 +         * @param msg
 +         *            the detail message
 +         */
 +        public NotNullableException(String msg) {
 +            super(msg);
 +        }
 +
 +        /**
 +         * Constructs a new <code>NotNullableException</code> from another
 +         * exception.
 +         * 
 +         * @param cause
 +         *            The cause of the failure
 +         */
 +        public NotNullableException(Throwable cause) {
 +            super(cause);
 +        }
 +    }
 +
 +    public void commit() {
 +        if (isModified()) {
 +            modified = false;
 +            value = changedValue;
 +        }
 +    }
 +}
index 78700caee9a7ca9c04b997999bc203cef799bc42,0000000000000000000000000000000000000000..f772e2701ccf85eedc9e15e0a5ae83323a2636f3
mode 100644,000000..100644
--- /dev/null
@@@ -1,1728 -1,0 +1,1746 @@@
-             itemProperties
-                     .add(new ColumnProperty(propertyId, propertyReadOnly
-                             .get(propertyId),
-                             !propertyReadOnly.get(propertyId), propertyNullable
-                                     .get(propertyId), null, getType(propertyId)));
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +package com.vaadin.data.util.sqlcontainer;
 +
 +import java.io.IOException;
 +import java.sql.ResultSet;
 +import java.sql.ResultSetMetaData;
 +import java.sql.SQLException;
 +import java.util.ArrayList;
 +import java.util.Collection;
 +import java.util.Collections;
 +import java.util.ConcurrentModificationException;
 +import java.util.Date;
 +import java.util.EventObject;
 +import java.util.HashMap;
 +import java.util.LinkedList;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.logging.Level;
 +import java.util.logging.Logger;
 +
 +import com.vaadin.data.Container;
 +import com.vaadin.data.Item;
 +import com.vaadin.data.Property;
 +import com.vaadin.data.util.filter.Compare.Equal;
 +import com.vaadin.data.util.filter.Like;
 +import com.vaadin.data.util.filter.UnsupportedFilterException;
 +import com.vaadin.data.util.sqlcontainer.query.OrderBy;
 +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate;
 +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate.RowIdChangeListener;
 +import com.vaadin.data.util.sqlcontainer.query.TableQuery;
 +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator;
 +import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator;
 +
 +public class SQLContainer implements Container, Container.Filterable,
 +        Container.Indexed, Container.Sortable, Container.ItemSetChangeNotifier {
 +
 +    /** Query delegate */
 +    private QueryDelegate delegate;
 +    /** Auto commit mode, default = false */
 +    private boolean autoCommit = false;
 +
 +    /** Page length = number of items contained in one page */
 +    private int pageLength = DEFAULT_PAGE_LENGTH;
 +    public static final int DEFAULT_PAGE_LENGTH = 100;
 +
 +    /** Number of items to cache = CACHE_RATIO x pageLength */
 +    public static final int CACHE_RATIO = 2;
 +
 +    /** Item and index caches */
 +    private final Map<Integer, RowId> itemIndexes = new HashMap<Integer, RowId>();
 +    private final CacheMap<RowId, RowItem> cachedItems = new CacheMap<RowId, RowItem>();
 +
 +    /** Container properties = column names, data types and statuses */
 +    private final List<String> propertyIds = new ArrayList<String>();
 +    private final Map<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>();
 +    private final Map<String, Boolean> propertyReadOnly = new HashMap<String, Boolean>();
++    private final Map<String, Boolean> propertyPersistable = new HashMap<String, Boolean>();
 +    private final Map<String, Boolean> propertyNullable = new HashMap<String, Boolean>();
++    private final Map<String, Boolean> propertyPrimaryKey = new HashMap<String, Boolean>();
 +
 +    /** Filters (WHERE) and sorters (ORDER BY) */
 +    private final List<Filter> filters = new ArrayList<Filter>();
 +    private final List<OrderBy> sorters = new ArrayList<OrderBy>();
 +
 +    /**
 +     * Total number of items available in the data source using the current
 +     * query, filters and sorters.
 +     */
 +    private int size;
 +
 +    /**
 +     * Size updating logic. Do not update size from data source if it has been
 +     * updated in the last sizeValidMilliSeconds milliseconds.
 +     */
 +    private final int sizeValidMilliSeconds = 10000;
 +    private boolean sizeDirty = true;
 +    private Date sizeUpdated = new Date();
 +
 +    /** Starting row number of the currently fetched page */
 +    private int currentOffset;
 +
 +    /** ItemSetChangeListeners */
 +    private LinkedList<Container.ItemSetChangeListener> itemSetChangeListeners;
 +
 +    /** Temporary storage for modified items and items to be removed and added */
 +    private final Map<RowId, RowItem> removedItems = new HashMap<RowId, RowItem>();
 +    private final List<RowItem> addedItems = new ArrayList<RowItem>();
 +    private final List<RowItem> modifiedItems = new ArrayList<RowItem>();
 +
 +    /** List of references to other SQLContainers */
 +    private final Map<SQLContainer, Reference> references = new HashMap<SQLContainer, Reference>();
 +
 +    /** Cache flush notification system enabled. Disabled by default. */
 +    private boolean notificationsEnabled;
 +
 +    /**
 +     * Prevent instantiation without a QueryDelegate.
 +     */
 +    @SuppressWarnings("unused")
 +    private SQLContainer() {
 +    }
 +
 +    /**
 +     * Creates and initializes SQLContainer using the given QueryDelegate
 +     * 
 +     * @param delegate
 +     *            QueryDelegate implementation
 +     * @throws SQLException
 +     */
 +    public SQLContainer(QueryDelegate delegate) throws SQLException {
 +        if (delegate == null) {
 +            throw new IllegalArgumentException(
 +                    "QueryDelegate must not be null.");
 +        }
 +        this.delegate = delegate;
 +        getPropertyIds();
 +        cachedItems.setCacheLimit(CACHE_RATIO * getPageLength());
 +    }
 +
 +    /**************************************/
 +    /** Methods from interface Container **/
 +    /**************************************/
 +
 +    /**
 +     * Note! If auto commit mode is enabled, this method will still return the
 +     * temporary row ID assigned for the item. Implement
 +     * QueryDelegate.RowIdChangeListener to receive the actual Row ID value
 +     * after the addition has been committed.
 +     * 
 +     * {@inheritDoc}
 +     */
 +
 +    @Override
 +    public Object addItem() throws UnsupportedOperationException {
 +        Object emptyKey[] = new Object[delegate.getPrimaryKeyColumns().size()];
 +        RowId itemId = new TemporaryRowId(emptyKey);
 +        // Create new empty column properties for the row item.
 +        List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>();
 +        for (String propertyId : propertyIds) {
 +            /* Default settings for new item properties. */
-         refresh();
++            ColumnProperty cp = new ColumnProperty(propertyId,
++                    propertyReadOnly.get(propertyId),
++                    propertyPersistable.get(propertyId),
++                    propertyNullable.get(propertyId),
++                    propertyPrimaryKey.get(propertyId), null,
++                    getType(propertyId));
++
++            itemProperties.add(cp);
 +        }
 +        RowItem newRowItem = new RowItem(this, itemId, itemProperties);
 +
 +        if (autoCommit) {
 +            /* Add and commit instantly */
 +            try {
 +                if (delegate instanceof TableQuery) {
 +                    itemId = ((TableQuery) delegate)
 +                            .storeRowImmediately(newRowItem);
 +                } else {
 +                    delegate.beginTransaction();
 +                    delegate.storeRow(newRowItem);
 +                    delegate.commit();
 +                }
 +                refresh();
 +                if (notificationsEnabled) {
 +                    CacheFlushNotifier.notifyOfCacheFlush(this);
 +                }
 +                getLogger().log(Level.FINER, "Row added to DB...");
 +                return itemId;
 +            } catch (SQLException e) {
 +                getLogger().log(Level.WARNING,
 +                        "Failed to add row to DB. Rolling back.", e);
 +                try {
 +                    delegate.rollback();
 +                } catch (SQLException ee) {
 +                    getLogger().log(Level.SEVERE,
 +                            "Failed to roll back row addition", e);
 +                }
 +                return null;
 +            }
 +        } else {
 +            addedItems.add(newRowItem);
 +            fireContentsChange();
 +            return itemId;
 +        }
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#containsId(java.lang.Object)
 +     */
 +
 +    @Override
 +    public boolean containsId(Object itemId) {
 +        if (itemId == null) {
 +            return false;
 +        }
 +
 +        if (cachedItems.containsKey(itemId)) {
 +            return true;
 +        } else {
 +            for (RowItem item : addedItems) {
 +                if (item.getId().equals(itemId)) {
 +                    return itemPassesFilters(item);
 +                }
 +            }
 +        }
 +        if (removedItems.containsKey(itemId)) {
 +            return false;
 +        }
 +
 +        if (itemId instanceof ReadOnlyRowId) {
 +            int rowNum = ((ReadOnlyRowId) itemId).getRowNum();
 +            return rowNum >= 0 && rowNum < size;
 +        }
 +
 +        if (itemId instanceof RowId && !(itemId instanceof TemporaryRowId)) {
 +            try {
 +                return delegate.containsRowWithKey(((RowId) itemId).getId());
 +            } catch (Exception e) {
 +                /* Query failed, just return false. */
 +                getLogger().log(Level.WARNING, "containsId query failed", e);
 +            }
 +        }
 +        return false;
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object,
 +     * java.lang.Object)
 +     */
 +
 +    @Override
 +    public Property<?> getContainerProperty(Object itemId, Object propertyId) {
 +        Item item = getItem(itemId);
 +        if (item == null) {
 +            return null;
 +        }
 +        return item.getItemProperty(propertyId);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#getContainerPropertyIds()
 +     */
 +
 +    @Override
 +    public Collection<?> getContainerPropertyIds() {
 +        return Collections.unmodifiableCollection(propertyIds);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#getItem(java.lang.Object)
 +     */
 +
 +    @Override
 +    public Item getItem(Object itemId) {
 +        if (!cachedItems.containsKey(itemId)) {
 +            int index = indexOfId(itemId);
 +            if (index >= size) {
 +                // The index is in the added items
 +                int offset = index - size;
 +                RowItem item = addedItems.get(offset);
 +                if (itemPassesFilters(item)) {
 +                    return item;
 +                } else {
 +                    return null;
 +                }
 +            } else {
 +                // load the item into cache
 +                updateOffsetAndCache(index);
 +            }
 +        }
 +        return cachedItems.get(itemId);
 +    }
 +
 +    /**
 +     * Bypasses in-memory filtering to return items that are cached in memory.
 +     * <em>NOTE</em>: This does not bypass database-level filtering.
 +     * 
 +     * @param itemId
 +     *            the id of the item to retrieve.
 +     * @return the item represented by itemId.
 +     */
 +    public Item getItemUnfiltered(Object itemId) {
 +        if (!cachedItems.containsKey(itemId)) {
 +            for (RowItem item : addedItems) {
 +                if (item.getId().equals(itemId)) {
 +                    return item;
 +                }
 +            }
 +        }
 +        return cachedItems.get(itemId);
 +    }
 +
 +    /**
 +     * NOTE! Do not use this method if in any way avoidable. This method doesn't
 +     * (and cannot) use lazy loading, which means that all rows in the database
 +     * will be loaded into memory.
 +     * 
 +     * {@inheritDoc}
 +     */
 +
 +    @Override
 +    public Collection<?> getItemIds() {
 +        updateCount();
 +        ArrayList<RowId> ids = new ArrayList<RowId>();
 +        ResultSet rs = null;
 +        try {
 +            // Load ALL rows :(
 +            delegate.beginTransaction();
 +            rs = delegate.getResults(0, 0);
 +            List<String> pKeys = delegate.getPrimaryKeyColumns();
 +            while (rs.next()) {
 +                RowId id = null;
 +                if (pKeys.isEmpty()) {
 +                    /* Create a read only itemId */
 +                    id = new ReadOnlyRowId(rs.getRow());
 +                } else {
 +                    /* Generate itemId for the row based on primary key(s) */
 +                    Object[] itemId = new Object[pKeys.size()];
 +                    for (int i = 0; i < pKeys.size(); i++) {
 +                        itemId[i] = rs.getObject(pKeys.get(i));
 +                    }
 +                    id = new RowId(itemId);
 +                }
 +                if (id != null && !removedItems.containsKey(id)) {
 +                    ids.add(id);
 +                }
 +            }
 +            rs.getStatement().close();
 +            rs.close();
 +            delegate.commit();
 +        } catch (SQLException e) {
 +            getLogger().log(Level.WARNING,
 +                    "getItemIds() failed, rolling back.", e);
 +            try {
 +                delegate.rollback();
 +            } catch (SQLException e1) {
 +                getLogger().log(Level.SEVERE, "Failed to roll back state", e1);
 +            }
 +            try {
 +                rs.getStatement().close();
 +                rs.close();
 +            } catch (SQLException e1) {
 +                getLogger().log(Level.WARNING, "Closing session failed", e1);
 +            }
 +            throw new RuntimeException("Failed to fetch item indexes.", e);
 +        }
 +        for (RowItem item : getFilteredAddedItems()) {
 +            ids.add(item.getId());
 +        }
 +        return Collections.unmodifiableCollection(ids);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#getType(java.lang.Object)
 +     */
 +
 +    @Override
 +    public Class<?> getType(Object propertyId) {
 +        if (!propertyIds.contains(propertyId)) {
 +            return null;
 +        }
 +        return propertyTypes.get(propertyId);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#size()
 +     */
 +
 +    @Override
 +    public int size() {
 +        updateCount();
 +        return size + sizeOfAddedItems() - removedItems.size();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#removeItem(java.lang.Object)
 +     */
 +
 +    @Override
 +    public boolean removeItem(Object itemId)
 +            throws UnsupportedOperationException {
 +        if (!containsId(itemId)) {
 +            return false;
 +        }
 +        for (RowItem item : addedItems) {
 +            if (item.getId().equals(itemId)) {
 +                addedItems.remove(item);
 +                fireContentsChange();
 +                return true;
 +            }
 +        }
 +
 +        if (autoCommit) {
 +            /* Remove and commit instantly. */
 +            Item i = getItem(itemId);
 +            if (i == null) {
 +                return false;
 +            }
 +            try {
 +                delegate.beginTransaction();
 +                boolean success = delegate.removeRow((RowItem) i);
 +                delegate.commit();
 +                refresh();
 +                if (notificationsEnabled) {
 +                    CacheFlushNotifier.notifyOfCacheFlush(this);
 +                }
 +                if (success) {
 +                    getLogger().log(Level.FINER, "Row removed from DB...");
 +                }
 +                return success;
 +            } catch (SQLException e) {
 +                getLogger().log(Level.WARNING,
 +                        "Failed to remove row, rolling back", e);
 +                try {
 +                    delegate.rollback();
 +                } catch (SQLException ee) {
 +                    /* Nothing can be done here */
 +                    getLogger().log(Level.SEVERE,
 +                            "Failed to rollback row removal", ee);
 +                }
 +                return false;
 +            } catch (OptimisticLockException e) {
 +                getLogger().log(Level.WARNING,
 +                        "Failed to remove row, rolling back", e);
 +                try {
 +                    delegate.rollback();
 +                } catch (SQLException ee) {
 +                    /* Nothing can be done here */
 +                    getLogger().log(Level.SEVERE,
 +                            "Failed to rollback row removal", ee);
 +                }
 +                throw e;
 +            }
 +        } else {
 +            removedItems.put((RowId) itemId, (RowItem) getItem(itemId));
 +            cachedItems.remove(itemId);
 +            refresh();
 +            return true;
 +        }
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#removeAllItems()
 +     */
 +
 +    @Override
 +    public boolean removeAllItems() throws UnsupportedOperationException {
 +        if (autoCommit) {
 +            /* Remove and commit instantly. */
 +            try {
 +                delegate.beginTransaction();
 +                boolean success = true;
 +                for (Object id : getItemIds()) {
 +                    if (!delegate.removeRow((RowItem) getItem(id))) {
 +                        success = false;
 +                    }
 +                }
 +                if (success) {
 +                    delegate.commit();
 +                    getLogger().log(Level.FINER, "All rows removed from DB...");
 +                    refresh();
 +                    if (notificationsEnabled) {
 +                        CacheFlushNotifier.notifyOfCacheFlush(this);
 +                    }
 +                } else {
 +                    delegate.rollback();
 +                }
 +                return success;
 +            } catch (SQLException e) {
 +                getLogger().log(Level.WARNING,
 +                        "removeAllItems() failed, rolling back", e);
 +                try {
 +                    delegate.rollback();
 +                } catch (SQLException ee) {
 +                    /* Nothing can be done here */
 +                    getLogger().log(Level.SEVERE, "Failed to roll back", ee);
 +                }
 +                return false;
 +            } catch (OptimisticLockException e) {
 +                getLogger().log(Level.WARNING,
 +                        "removeAllItems() failed, rolling back", e);
 +                try {
 +                    delegate.rollback();
 +                } catch (SQLException ee) {
 +                    /* Nothing can be done here */
 +                    getLogger().log(Level.SEVERE, "Failed to roll back", ee);
 +                }
 +                throw e;
 +            }
 +        } else {
 +            for (Object id : getItemIds()) {
 +                removedItems.put((RowId) id, (RowItem) getItem(id));
 +                cachedItems.remove(id);
 +            }
 +            refresh();
 +            return true;
 +        }
 +    }
 +
 +    /*************************************************/
 +    /** Methods from interface Container.Filterable **/
 +    /*************************************************/
 +
 +    /**
 +     * {@inheritDoc}
 +     */
 +
 +    @Override
 +    public void addContainerFilter(Filter filter)
 +            throws UnsupportedFilterException {
 +        // filter.setCaseSensitive(!ignoreCase);
 +
 +        filters.add(filter);
 +        refresh();
 +    }
 +
 +    /**
 +     * {@inheritDoc}
 +     */
 +
 +    @Override
 +    public void removeContainerFilter(Filter filter) {
 +        filters.remove(filter);
-                 if (delegate instanceof TableQuery
-                         && rsmd.getColumnLabel(i).equals(
-                                 ((TableQuery) delegate).getVersionColumn())) {
-                     readOnly = true;
++        // TODO this cannot be added before ComboBox is fixed
++        // (Select.requestRepaint() must not affect filter string)
++        // refresh();
 +    }
 +
 +    /**
 +     * {@inheritDoc}
 +     */
 +    public void addContainerFilter(Object propertyId, String filterString,
 +            boolean ignoreCase, boolean onlyMatchPrefix) {
 +        if (propertyId == null || !propertyIds.contains(propertyId)) {
 +            return;
 +        }
 +
 +        /* Generate Filter -object */
 +        String likeStr = onlyMatchPrefix ? filterString + "%" : "%"
 +                + filterString + "%";
 +        Like like = new Like(propertyId.toString(), likeStr);
 +        like.setCaseSensitive(!ignoreCase);
 +        filters.add(like);
 +        refresh();
 +    }
 +
 +    /**
 +     * {@inheritDoc}
 +     */
 +    public void removeContainerFilters(Object propertyId) {
 +        ArrayList<Filter> toRemove = new ArrayList<Filter>();
 +        for (Filter f : filters) {
 +            if (f.appliesToProperty(propertyId)) {
 +                toRemove.add(f);
 +            }
 +        }
 +        filters.removeAll(toRemove);
 +        refresh();
 +    }
 +
 +    /**
 +     * {@inheritDoc}
 +     */
 +
 +    @Override
 +    public void removeAllContainerFilters() {
 +        filters.clear();
 +        refresh();
 +    }
 +
 +    /**********************************************/
 +    /** Methods from interface Container.Indexed **/
 +    /**********************************************/
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object)
 +     */
 +
 +    @Override
 +    public int indexOfId(Object itemId) {
 +        // First check if the id is in the added items
 +        for (int ix = 0; ix < addedItems.size(); ix++) {
 +            RowItem item = addedItems.get(ix);
 +            if (item.getId().equals(itemId)) {
 +                if (itemPassesFilters(item)) {
 +                    updateCount();
 +                    return size + ix;
 +                } else {
 +                    return -1;
 +                }
 +            }
 +        }
 +
 +        if (!containsId(itemId)) {
 +            return -1;
 +        }
 +        if (cachedItems.isEmpty()) {
 +            getPage();
 +        }
 +        int size = size();
 +        boolean wrappedAround = false;
 +        while (!wrappedAround) {
 +            for (Integer i : itemIndexes.keySet()) {
 +                if (itemIndexes.get(i).equals(itemId)) {
 +                    return i;
 +                }
 +            }
 +            // load in the next page.
 +            int nextIndex = (currentOffset / (pageLength * CACHE_RATIO) + 1)
 +                    * (pageLength * CACHE_RATIO);
 +            if (nextIndex >= size) {
 +                // Container wrapped around, start from index 0.
 +                wrappedAround = true;
 +                nextIndex = 0;
 +            }
 +            updateOffsetAndCache(nextIndex);
 +        }
 +        return -1;
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Indexed#getIdByIndex(int)
 +     */
 +
 +    @Override
 +    public Object getIdByIndex(int index) {
 +        if (index < 0 || index > size() - 1) {
 +            return null;
 +        }
 +        if (index < size) {
 +            if (itemIndexes.keySet().contains(index)) {
 +                return itemIndexes.get(index);
 +            }
 +            updateOffsetAndCache(index);
 +            return itemIndexes.get(index);
 +        } else {
 +            // The index is in the added items
 +            int offset = index - size;
 +            return addedItems.get(offset).getId();
 +        }
 +    }
 +
 +    /**********************************************/
 +    /** Methods from interface Container.Ordered **/
 +    /**********************************************/
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object)
 +     */
 +
 +    @Override
 +    public Object nextItemId(Object itemId) {
 +        return getIdByIndex(indexOfId(itemId) + 1);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object)
 +     */
 +
 +    @Override
 +    public Object prevItemId(Object itemId) {
 +        return getIdByIndex(indexOfId(itemId) - 1);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Ordered#firstItemId()
 +     */
 +
 +    @Override
 +    public Object firstItemId() {
 +        updateCount();
 +        if (size == 0) {
 +            if (addedItems.isEmpty()) {
 +                return null;
 +            } else {
 +                int ix = -1;
 +                do {
 +                    ix++;
 +                } while (!itemPassesFilters(addedItems.get(ix))
 +                        && ix < addedItems.size());
 +                if (ix < addedItems.size()) {
 +                    return addedItems.get(ix).getId();
 +                }
 +            }
 +        }
 +        if (!itemIndexes.containsKey(0)) {
 +            updateOffsetAndCache(0);
 +        }
 +        return itemIndexes.get(0);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Ordered#lastItemId()
 +     */
 +
 +    @Override
 +    public Object lastItemId() {
 +        if (addedItems.isEmpty()) {
 +            int lastIx = size() - 1;
 +            if (!itemIndexes.containsKey(lastIx)) {
 +                updateOffsetAndCache(size - 1);
 +            }
 +            return itemIndexes.get(lastIx);
 +        } else {
 +            int ix = addedItems.size();
 +            do {
 +                ix--;
 +            } while (!itemPassesFilters(addedItems.get(ix)) && ix >= 0);
 +            if (ix >= 0) {
 +                return addedItems.get(ix).getId();
 +            } else {
 +                return null;
 +            }
 +        }
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object)
 +     */
 +
 +    @Override
 +    public boolean isFirstId(Object itemId) {
 +        return firstItemId().equals(itemId);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object)
 +     */
 +
 +    @Override
 +    public boolean isLastId(Object itemId) {
 +        return lastItemId().equals(itemId);
 +    }
 +
 +    /***********************************************/
 +    /** Methods from interface Container.Sortable **/
 +    /***********************************************/
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
 +     * boolean[])
 +     */
 +
 +    @Override
 +    public void sort(Object[] propertyId, boolean[] ascending) {
 +        sorters.clear();
 +        if (propertyId == null || propertyId.length == 0) {
 +            refresh();
 +            return;
 +        }
 +        /* Generate OrderBy -objects */
 +        boolean asc = true;
 +        for (int i = 0; i < propertyId.length; i++) {
 +            /* Check that the property id is valid */
 +            if (propertyId[i] instanceof String
 +                    && propertyIds.contains(propertyId[i])) {
 +                try {
 +                    asc = ascending[i];
 +                } catch (Exception e) {
 +                    getLogger().log(Level.WARNING, "", e);
 +                }
 +                sorters.add(new OrderBy((String) propertyId[i], asc));
 +            }
 +        }
 +        refresh();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds()
 +     */
 +
 +    @Override
 +    public Collection<?> getSortableContainerPropertyIds() {
 +        return getContainerPropertyIds();
 +    }
 +
 +    /**************************************/
 +    /** Methods specific to SQLContainer **/
 +    /**************************************/
 +
 +    /**
 +     * Refreshes the container - clears all caches and resets size and offset.
 +     * Does NOT remove sorting or filtering rules!
 +     */
 +    public void refresh() {
 +        sizeDirty = true;
 +        currentOffset = 0;
 +        cachedItems.clear();
 +        itemIndexes.clear();
 +        fireContentsChange();
 +    }
 +
 +    /**
 +     * Returns modify state of the container.
 +     * 
 +     * @return true if contents of this container have been modified
 +     */
 +    public boolean isModified() {
 +        return !removedItems.isEmpty() || !addedItems.isEmpty()
 +                || !modifiedItems.isEmpty();
 +    }
 +
 +    /**
 +     * Set auto commit mode enabled or disabled. Auto commit mode means that all
 +     * changes made to items of this container will be immediately written to
 +     * the underlying data source.
 +     * 
 +     * @param autoCommitEnabled
 +     *            true to enable auto commit mode
 +     */
 +    public void setAutoCommit(boolean autoCommitEnabled) {
 +        autoCommit = autoCommitEnabled;
 +    }
 +
 +    /**
 +     * Returns status of the auto commit mode.
 +     * 
 +     * @return true if auto commit mode is enabled
 +     */
 +    public boolean isAutoCommit() {
 +        return autoCommit;
 +    }
 +
 +    /**
 +     * Returns the currently set page length.
 +     * 
 +     * @return current page length
 +     */
 +    public int getPageLength() {
 +        return pageLength;
 +    }
 +
 +    /**
 +     * Sets the page length used in lazy fetching of items from the data source.
 +     * Also resets the cache size to match the new page length.
 +     * 
 +     * As a side effect the container will be refreshed.
 +     * 
 +     * @param pageLength
 +     *            new page length
 +     */
 +    public void setPageLength(int pageLength) {
 +        setPageLengthInternal(pageLength);
 +        refresh();
 +    }
 +
 +    /**
 +     * Sets the page length internally, without refreshing the container.
 +     * 
 +     * @param pageLength
 +     *            the new page length
 +     */
 +    private void setPageLengthInternal(int pageLength) {
 +        this.pageLength = pageLength > 0 ? pageLength : DEFAULT_PAGE_LENGTH;
 +        cachedItems.setCacheLimit(CACHE_RATIO * getPageLength());
 +    }
 +
 +    /**
 +     * Adds the given OrderBy to this container and refreshes the container
 +     * contents with the new sorting rules.
 +     * 
 +     * Note that orderBy.getColumn() must return a column name that exists in
 +     * this container.
 +     * 
 +     * @param orderBy
 +     *            OrderBy to be added to the container sorting rules
 +     */
 +    public void addOrderBy(OrderBy orderBy) {
 +        if (orderBy == null) {
 +            return;
 +        }
 +        if (!propertyIds.contains(orderBy.getColumn())) {
 +            throw new IllegalArgumentException(
 +                    "The column given for sorting does not exist in this container.");
 +        }
 +        sorters.add(orderBy);
 +        refresh();
 +    }
 +
 +    /**
 +     * Commits all the changes, additions and removals made to the items of this
 +     * container.
 +     * 
 +     * @throws UnsupportedOperationException
 +     * @throws SQLException
 +     */
 +    public void commit() throws UnsupportedOperationException, SQLException {
 +        try {
 +            getLogger().log(Level.FINER,
 +                    "Commiting changes through delegate...");
 +            delegate.beginTransaction();
 +            /* Perform buffered deletions */
 +            for (RowItem item : removedItems.values()) {
 +                if (!delegate.removeRow(item)) {
 +                    throw new SQLException("Removal failed for row with ID: "
 +                            + item.getId());
 +                }
 +            }
 +            /* Perform buffered modifications */
 +            for (RowItem item : modifiedItems) {
 +                if (delegate.storeRow(item) > 0) {
 +                    /*
 +                     * Also reset the modified state in the item in case it is
 +                     * reused e.g. in a form.
 +                     */
 +                    item.commit();
 +                } else {
 +                    delegate.rollback();
 +                    refresh();
 +                    throw new ConcurrentModificationException(
 +                            "Item with the ID '" + item.getId()
 +                                    + "' has been externally modified.");
 +                }
 +            }
 +            /* Perform buffered additions */
 +            for (RowItem item : addedItems) {
 +                delegate.storeRow(item);
 +            }
 +            delegate.commit();
 +            removedItems.clear();
 +            addedItems.clear();
 +            modifiedItems.clear();
 +            refresh();
 +            if (notificationsEnabled) {
 +                CacheFlushNotifier.notifyOfCacheFlush(this);
 +            }
 +        } catch (SQLException e) {
 +            delegate.rollback();
 +            throw e;
 +        } catch (OptimisticLockException e) {
 +            delegate.rollback();
 +            throw e;
 +        }
 +    }
 +
 +    /**
 +     * Rolls back all the changes, additions and removals made to the items of
 +     * this container.
 +     * 
 +     * @throws UnsupportedOperationException
 +     * @throws SQLException
 +     */
 +    public void rollback() throws UnsupportedOperationException, SQLException {
 +        getLogger().log(Level.FINE, "Rolling back changes...");
 +        removedItems.clear();
 +        addedItems.clear();
 +        modifiedItems.clear();
 +        refresh();
 +    }
 +
 +    /**
 +     * Notifies this container that a property in the given item has been
 +     * modified. The change will be buffered or made instantaneously depending
 +     * on auto commit mode.
 +     * 
 +     * @param changedItem
 +     *            item that has a modified property
 +     */
 +    void itemChangeNotification(RowItem changedItem) {
 +        if (autoCommit) {
 +            try {
 +                delegate.beginTransaction();
 +                if (delegate.storeRow(changedItem) == 0) {
 +                    delegate.rollback();
 +                    refresh();
 +                    throw new ConcurrentModificationException(
 +                            "Item with the ID '" + changedItem.getId()
 +                                    + "' has been externally modified.");
 +                }
 +                delegate.commit();
 +                if (notificationsEnabled) {
 +                    CacheFlushNotifier.notifyOfCacheFlush(this);
 +                }
 +                getLogger().log(Level.FINER, "Row updated to DB...");
 +            } catch (SQLException e) {
 +                getLogger().log(Level.WARNING,
 +                        "itemChangeNotification failed, rolling back...", e);
 +                try {
 +                    delegate.rollback();
 +                } catch (SQLException ee) {
 +                    /* Nothing can be done here */
 +                    getLogger().log(Level.SEVERE, "Rollback failed", e);
 +                }
 +                throw new RuntimeException(e);
 +            }
 +        } else {
 +            if (!(changedItem.getId() instanceof TemporaryRowId)
 +                    && !modifiedItems.contains(changedItem)) {
 +                modifiedItems.add(changedItem);
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Determines a new offset for updating the row cache. The offset is
 +     * calculated from the given index, and will be fixed to match the start of
 +     * a page, based on the value of pageLength.
 +     * 
 +     * @param index
 +     *            Index of the item that was requested, but not found in cache
 +     */
 +    private void updateOffsetAndCache(int index) {
 +        if (itemIndexes.containsKey(index)) {
 +            return;
 +        }
 +        currentOffset = (index / (pageLength * CACHE_RATIO))
 +                * (pageLength * CACHE_RATIO);
 +        if (currentOffset < 0) {
 +            currentOffset = 0;
 +        }
 +        getPage();
 +    }
 +
 +    /**
 +     * Fetches new count of rows from the data source, if needed.
 +     */
 +    private void updateCount() {
 +        if (!sizeDirty
 +                && new Date().getTime() < sizeUpdated.getTime()
 +                        + sizeValidMilliSeconds) {
 +            return;
 +        }
 +        try {
 +            try {
 +                delegate.setFilters(filters);
 +            } catch (UnsupportedOperationException e) {
 +                getLogger().log(Level.FINE,
 +                        "The query delegate doesn't support filtering", e);
 +            }
 +            try {
 +                delegate.setOrderBy(sorters);
 +            } catch (UnsupportedOperationException e) {
 +                getLogger().log(Level.FINE,
 +                        "The query delegate doesn't support filtering", e);
 +            }
 +            int newSize = delegate.getCount();
 +            if (newSize != size) {
 +                size = newSize;
 +                refresh();
 +            }
 +            sizeUpdated = new Date();
 +            sizeDirty = false;
 +            getLogger().log(Level.FINER,
 +                    "Updated row count. New count is: " + size);
 +        } catch (SQLException e) {
 +            throw new RuntimeException("Failed to update item set size.", e);
 +        }
 +    }
 +
 +    /**
 +     * Fetches property id's (column names and their types) from the data
 +     * source.
 +     * 
 +     * @throws SQLException
 +     */
 +    private void getPropertyIds() throws SQLException {
 +        propertyIds.clear();
 +        propertyTypes.clear();
 +        delegate.setFilters(null);
 +        delegate.setOrderBy(null);
 +        ResultSet rs = null;
 +        ResultSetMetaData rsmd = null;
 +        try {
 +            delegate.beginTransaction();
 +            rs = delegate.getResults(0, 1);
 +            boolean resultExists = rs.next();
 +            rsmd = rs.getMetaData();
 +            Class<?> type = null;
 +            for (int i = 1; i <= rsmd.getColumnCount(); i++) {
 +                if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) {
 +                    continue;
 +                }
 +                String colName = rsmd.getColumnLabel(i);
 +                /*
 +                 * Make sure not to add the same colName twice. This can easily
 +                 * happen if the SQL query joins many tables with an ID column.
 +                 */
 +                if (!propertyIds.contains(colName)) {
 +                    propertyIds.add(colName);
 +                }
 +                /* Try to determine the column's JDBC class by all means. */
 +                if (resultExists && rs.getObject(i) != null) {
 +                    type = rs.getObject(i).getClass();
 +                } else {
 +                    try {
 +                        type = Class.forName(rsmd.getColumnClassName(i));
 +                    } catch (Exception e) {
 +                        getLogger().log(Level.WARNING, "Class not found", e);
 +                        /* On failure revert to Object and hope for the best. */
 +                        type = Object.class;
 +                    }
 +                }
 +                /*
 +                 * Determine read only and nullability status of the column. A
 +                 * column is read only if it is reported as either read only or
 +                 * auto increment by the database, and also it is set as the
 +                 * version column in a TableQuery delegate.
 +                 */
 +                boolean readOnly = rsmd.isAutoIncrement(i)
 +                        || rsmd.isReadOnly(i);
-                                     !propertyReadOnly.get(colName),
-                                     propertyNullable.get(colName), value, type);
++
++                boolean persistable = !rsmd.isReadOnly(i);
++
++                if (delegate instanceof TableQuery) {
++                    if (rsmd.getColumnLabel(i).equals(
++                            ((TableQuery) delegate).getVersionColumn())) {
++                        readOnly = true;
++                    }
 +                }
++
 +                propertyReadOnly.put(colName, readOnly);
++                propertyPersistable.put(colName, persistable);
 +                propertyNullable.put(colName,
 +                        rsmd.isNullable(i) == ResultSetMetaData.columnNullable);
++                propertyPrimaryKey.put(colName, delegate.getPrimaryKeyColumns()
++                        .contains(rsmd.getColumnLabel(i)));
 +                propertyTypes.put(colName, type);
 +            }
 +            rs.getStatement().close();
 +            rs.close();
 +            delegate.commit();
 +            getLogger().log(Level.FINER, "Property IDs fetched.");
 +        } catch (SQLException e) {
 +            getLogger().log(Level.WARNING,
 +                    "Failed to fetch property ids, rolling back", e);
 +            try {
 +                delegate.rollback();
 +            } catch (SQLException e1) {
 +                getLogger().log(Level.SEVERE, "Failed to roll back", e1);
 +            }
 +            try {
 +                if (rs != null) {
 +                    if (rs.getStatement() != null) {
 +                        rs.getStatement().close();
 +                    }
 +                    rs.close();
 +                }
 +            } catch (SQLException e1) {
 +                getLogger().log(Level.WARNING, "Failed to close session", e1);
 +            }
 +            throw e;
 +        }
 +    }
 +
 +    /**
 +     * Fetches a page from the data source based on the values of pageLenght and
 +     * currentOffset. Also updates the set of primary keys, used in
 +     * identification of RowItems.
 +     */
 +    private void getPage() {
 +        updateCount();
 +        ResultSet rs = null;
 +        ResultSetMetaData rsmd = null;
 +        cachedItems.clear();
 +        itemIndexes.clear();
 +        try {
 +            try {
 +                delegate.setOrderBy(sorters);
 +            } catch (UnsupportedOperationException e) {
 +                /* The query delegate doesn't support sorting. */
 +                /* No need to do anything. */
 +                getLogger().log(Level.FINE,
 +                        "The query delegate doesn't support sorting", e);
 +            }
 +            delegate.beginTransaction();
 +            rs = delegate.getResults(currentOffset, pageLength * CACHE_RATIO);
 +            rsmd = rs.getMetaData();
 +            List<String> pKeys = delegate.getPrimaryKeyColumns();
 +            // }
 +            /* Create new items and column properties */
 +            ColumnProperty cp = null;
 +            int rowCount = currentOffset;
 +            if (!delegate.implementationRespectsPagingLimits()) {
 +                rowCount = currentOffset = 0;
 +                setPageLengthInternal(size);
 +            }
 +            while (rs.next()) {
 +                List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>();
 +                /* Generate row itemId based on primary key(s) */
 +                Object[] itemId = new Object[pKeys.size()];
 +                for (int i = 0; i < pKeys.size(); i++) {
 +                    itemId[i] = rs.getObject(pKeys.get(i));
 +                }
 +                RowId id = null;
 +                if (pKeys.isEmpty()) {
 +                    id = new ReadOnlyRowId(rs.getRow());
 +                } else {
 +                    id = new RowId(itemId);
 +                }
 +                List<String> propertiesToAdd = new ArrayList<String>(
 +                        propertyIds);
 +                if (!removedItems.containsKey(id)) {
 +                    for (int i = 1; i <= rsmd.getColumnCount(); i++) {
 +                        if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) {
 +                            continue;
 +                        }
 +                        String colName = rsmd.getColumnLabel(i);
 +                        Object value = rs.getObject(i);
 +                        Class<?> type = value != null ? value.getClass()
 +                                : Object.class;
 +                        if (value == null) {
 +                            for (String propName : propertyTypes.keySet()) {
 +                                if (propName.equals(rsmd.getColumnLabel(i))) {
 +                                    type = propertyTypes.get(propName);
 +                                    break;
 +                                }
 +                            }
 +                        }
 +                        /*
 +                         * In case there are more than one column with the same
 +                         * name, add only the first one. This can easily happen
 +                         * if you join many tables where each table has an ID
 +                         * column.
 +                         */
 +                        if (propertiesToAdd.contains(colName)) {
++
 +                            cp = new ColumnProperty(colName,
 +                                    propertyReadOnly.get(colName),
++                                    propertyPersistable.get(colName),
++                                    propertyNullable.get(colName),
++                                    propertyPrimaryKey.get(colName), value,
++                                    type);
 +                            itemProperties.add(cp);
 +                            propertiesToAdd.remove(colName);
 +                        }
 +                    }
 +                    /* Cache item */
 +                    itemIndexes.put(rowCount, id);
 +
 +                    // if an item with the id is contained in the modified
 +                    // cache, then use this record and add it to the cached
 +                    // items. Otherwise create a new item
 +                    int modifiedIndex = indexInModifiedCache(id);
 +                    if (modifiedIndex != -1) {
 +                        cachedItems.put(id, modifiedItems.get(modifiedIndex));
 +                    } else {
 +                        cachedItems.put(id, new RowItem(this, id,
 +                                itemProperties));
 +                    }
 +
 +                    rowCount++;
 +                }
 +            }
 +            rs.getStatement().close();
 +            rs.close();
 +            delegate.commit();
 +            getLogger().log(
 +                    Level.FINER,
 +                    "Fetched " + pageLength * CACHE_RATIO
 +                            + " rows starting from " + currentOffset);
 +        } catch (SQLException e) {
 +            getLogger().log(Level.WARNING,
 +                    "Failed to fetch rows, rolling back", e);
 +            try {
 +                delegate.rollback();
 +            } catch (SQLException e1) {
 +                getLogger().log(Level.SEVERE, "Failed to roll back", e1);
 +            }
 +            try {
 +                if (rs != null) {
 +                    if (rs.getStatement() != null) {
 +                        rs.getStatement().close();
 +                        rs.close();
 +                    }
 +                }
 +            } catch (SQLException e1) {
 +                getLogger().log(Level.WARNING, "Failed to close session", e1);
 +            }
 +            throw new RuntimeException("Failed to fetch page.", e);
 +        }
 +    }
 +
 +    /**
 +     * Returns the index of the item with the given itemId for the modified
 +     * cache.
 +     * 
 +     * @param itemId
 +     * @return the index of the item with the itemId in the modified cache. Or
 +     *         -1 if not found.
 +     */
 +    private int indexInModifiedCache(Object itemId) {
 +        for (int ix = 0; ix < modifiedItems.size(); ix++) {
 +            RowItem item = modifiedItems.get(ix);
 +            if (item.getId().equals(itemId)) {
 +                return ix;
 +            }
 +        }
 +        return -1;
 +    }
 +
 +    private int sizeOfAddedItems() {
 +        return getFilteredAddedItems().size();
 +    }
 +
 +    private List<RowItem> getFilteredAddedItems() {
 +        ArrayList<RowItem> filtered = new ArrayList<RowItem>(addedItems);
 +        if (filters != null && !filters.isEmpty()) {
 +            for (RowItem item : addedItems) {
 +                if (!itemPassesFilters(item)) {
 +                    filtered.remove(item);
 +                }
 +            }
 +        }
 +        return filtered;
 +    }
 +
 +    private boolean itemPassesFilters(RowItem item) {
 +        for (Filter filter : filters) {
 +            if (!filter.passesFilter(item.getId(), item)) {
 +                return false;
 +            }
 +        }
 +        return true;
 +    }
 +
 +    /**
 +     * Checks is the given column identifier valid to be used with SQLContainer.
 +     * Currently the only non-valid identifier is "rownum" when MSSQL or Oracle
 +     * is used. This is due to the way the SELECT queries are constructed in
 +     * order to implement paging in these databases.
 +     * 
 +     * @param identifier
 +     *            Column identifier
 +     * @return true if the identifier is valid
 +     */
 +    private boolean isColumnIdentifierValid(String identifier) {
 +        if (identifier.equalsIgnoreCase("rownum")
 +                && delegate instanceof TableQuery) {
 +            TableQuery tq = (TableQuery) delegate;
 +            if (tq.getSqlGenerator() instanceof MSSQLGenerator
 +                    || tq.getSqlGenerator() instanceof OracleGenerator) {
 +                return false;
 +            }
 +        }
 +        return true;
 +    }
 +
 +    /**
 +     * Returns the QueryDelegate set for this SQLContainer.
 +     * 
 +     * @return current querydelegate
 +     */
 +    protected QueryDelegate getQueryDelegate() {
 +        return delegate;
 +    }
 +
 +    /************************************/
 +    /** UNSUPPORTED CONTAINER FEATURES **/
 +    /************************************/
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
 +     * java.lang.Class, java.lang.Object)
 +     */
 +
 +    @Override
 +    public boolean addContainerProperty(Object propertyId, Class<?> type,
 +            Object defaultValue) throws UnsupportedOperationException {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object)
 +     */
 +
 +    @Override
 +    public boolean removeContainerProperty(Object propertyId)
 +            throws UnsupportedOperationException {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container#addItem(java.lang.Object)
 +     */
 +
 +    @Override
 +    public Item addItem(Object itemId) throws UnsupportedOperationException {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
 +     * java.lang.Object)
 +     */
 +
 +    @Override
 +    public Item addItemAfter(Object previousItemId, Object newItemId)
 +            throws UnsupportedOperationException {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object)
 +     */
 +
 +    @Override
 +    public Item addItemAt(int index, Object newItemId)
 +            throws UnsupportedOperationException {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Indexed#addItemAt(int)
 +     */
 +
 +    @Override
 +    public Object addItemAt(int index) throws UnsupportedOperationException {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
 +     */
 +
 +    @Override
 +    public Object addItemAfter(Object previousItemId)
 +            throws UnsupportedOperationException {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    /******************************************/
 +    /** ITEMSETCHANGENOTIFIER IMPLEMENTATION **/
 +    /******************************************/
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin
 +     * .data.Container.ItemSetChangeListener)
 +     */
 +
 +    @Override
 +    public void addListener(Container.ItemSetChangeListener listener) {
 +        if (itemSetChangeListeners == null) {
 +            itemSetChangeListeners = new LinkedList<Container.ItemSetChangeListener>();
 +        }
 +        itemSetChangeListeners.add(listener);
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see
 +     * com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin
 +     * .data.Container.ItemSetChangeListener)
 +     */
 +
 +    @Override
 +    public void removeListener(Container.ItemSetChangeListener listener) {
 +        if (itemSetChangeListeners != null) {
 +            itemSetChangeListeners.remove(listener);
 +        }
 +    }
 +
 +    protected void fireContentsChange() {
 +        if (itemSetChangeListeners != null) {
 +            final Object[] l = itemSetChangeListeners.toArray();
 +            final Container.ItemSetChangeEvent event = new SQLContainer.ItemSetChangeEvent(
 +                    this);
 +            for (int i = 0; i < l.length; i++) {
 +                ((Container.ItemSetChangeListener) l[i])
 +                        .containerItemSetChange(event);
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Simple ItemSetChangeEvent implementation.
 +     */
 +    @SuppressWarnings("serial")
 +    public static class ItemSetChangeEvent extends EventObject implements
 +            Container.ItemSetChangeEvent {
 +
 +        private ItemSetChangeEvent(SQLContainer source) {
 +            super(source);
 +        }
 +
 +        @Override
 +        public Container getContainer() {
 +            return (Container) getSource();
 +        }
 +    }
 +
 +    /**************************************************/
 +    /** ROWIDCHANGELISTENER PASSING TO QUERYDELEGATE **/
 +    /**************************************************/
 +
 +    /**
 +     * Adds a RowIdChangeListener to the QueryDelegate
 +     * 
 +     * @param listener
 +     */
 +    public void addListener(RowIdChangeListener listener) {
 +        if (delegate instanceof QueryDelegate.RowIdChangeNotifier) {
 +            ((QueryDelegate.RowIdChangeNotifier) delegate)
 +                    .addListener(listener);
 +        }
 +    }
 +
 +    /**
 +     * Removes a RowIdChangeListener from the QueryDelegate
 +     * 
 +     * @param listener
 +     */
 +    public void removeListener(RowIdChangeListener listener) {
 +        if (delegate instanceof QueryDelegate.RowIdChangeNotifier) {
 +            ((QueryDelegate.RowIdChangeNotifier) delegate)
 +                    .removeListener(listener);
 +        }
 +    }
 +
 +    /**
 +     * Calling this will enable this SQLContainer to send and receive cache
 +     * flush notifications for its lifetime.
 +     */
 +    public void enableCacheFlushNotifications() {
 +        if (!notificationsEnabled) {
 +            notificationsEnabled = true;
 +            CacheFlushNotifier.addInstance(this);
 +        }
 +    }
 +
 +    /******************************************/
 +    /** Referencing mechanism implementation **/
 +    /******************************************/
 +
 +    /**
 +     * Adds a new reference to the given SQLContainer. In addition to the
 +     * container you must provide the column (property) names used for the
 +     * reference in both this and the referenced SQLContainer.
 +     * 
 +     * Note that multiple references pointing to the same SQLContainer are not
 +     * supported.
 +     * 
 +     * @param refdCont
 +     *            Target SQLContainer of the new reference
 +     * @param refingCol
 +     *            Column (property) name in this container storing the (foreign
 +     *            key) reference
 +     * @param refdCol
 +     *            Column (property) name in the referenced container storing the
 +     *            referenced key
 +     */
 +    public void addReference(SQLContainer refdCont, String refingCol,
 +            String refdCol) {
 +        if (refdCont == null) {
 +            throw new IllegalArgumentException(
 +                    "Referenced SQLContainer can not be null.");
 +        }
 +        if (!getContainerPropertyIds().contains(refingCol)) {
 +            throw new IllegalArgumentException(
 +                    "Given referencing column name is invalid."
 +                            + " Please ensure that this container"
 +                            + " contains a property ID named: " + refingCol);
 +        }
 +        if (!refdCont.getContainerPropertyIds().contains(refdCol)) {
 +            throw new IllegalArgumentException(
 +                    "Given referenced column name is invalid."
 +                            + " Please ensure that the referenced container"
 +                            + " contains a property ID named: " + refdCol);
 +        }
 +        if (references.keySet().contains(refdCont)) {
 +            throw new IllegalArgumentException(
 +                    "An SQLContainer instance can only be referenced once.");
 +        }
 +        references.put(refdCont, new Reference(refdCont, refingCol, refdCol));
 +    }
 +
 +    /**
 +     * Removes the reference pointing to the given SQLContainer.
 +     * 
 +     * @param refdCont
 +     *            Target SQLContainer of the reference
 +     * @return true if successful, false if the reference did not exist
 +     */
 +    public boolean removeReference(SQLContainer refdCont) {
 +        if (refdCont == null) {
 +            throw new IllegalArgumentException(
 +                    "Referenced SQLContainer can not be null.");
 +        }
 +        return references.remove(refdCont) == null ? false : true;
 +    }
 +
 +    /**
 +     * Sets the referenced item. The referencing column of the item in this
 +     * container is updated accordingly.
 +     * 
 +     * @param itemId
 +     *            Item Id of the reference source (from this container)
 +     * @param refdItemId
 +     *            Item Id of the reference target (from referenced container)
 +     * @param refdCont
 +     *            Target SQLContainer of the reference
 +     * @return true if the referenced item was successfully set, false on
 +     *         failure
 +     */
 +    public boolean setReferencedItem(Object itemId, Object refdItemId,
 +            SQLContainer refdCont) {
 +        if (refdCont == null) {
 +            throw new IllegalArgumentException(
 +                    "Referenced SQLContainer can not be null.");
 +        }
 +        Reference r = references.get(refdCont);
 +        if (r == null) {
 +            throw new IllegalArgumentException(
 +                    "Reference to the given SQLContainer not defined.");
 +        }
 +        try {
 +            getContainerProperty(itemId, r.getReferencingColumn()).setValue(
 +                    refdCont.getContainerProperty(refdItemId,
 +                            r.getReferencedColumn()));
 +            return true;
 +        } catch (Exception e) {
 +            getLogger()
 +                    .log(Level.WARNING, "Setting referenced item failed.", e);
 +            return false;
 +        }
 +    }
 +
 +    /**
 +     * Fetches the Item Id of the referenced item from the target SQLContainer.
 +     * 
 +     * @param itemId
 +     *            Item Id of the reference source (from this container)
 +     * @param refdCont
 +     *            Target SQLContainer of the reference
 +     * @return Item Id of the referenced item, or null if not found
 +     */
 +    public Object getReferencedItemId(Object itemId, SQLContainer refdCont) {
 +        if (refdCont == null) {
 +            throw new IllegalArgumentException(
 +                    "Referenced SQLContainer can not be null.");
 +        }
 +        Reference r = references.get(refdCont);
 +        if (r == null) {
 +            throw new IllegalArgumentException(
 +                    "Reference to the given SQLContainer not defined.");
 +        }
 +        Object refKey = getContainerProperty(itemId, r.getReferencingColumn())
 +                .getValue();
 +
 +        refdCont.removeAllContainerFilters();
 +        refdCont.addContainerFilter(new Equal(r.getReferencedColumn(), refKey));
 +        Object toReturn = refdCont.firstItemId();
 +        refdCont.removeAllContainerFilters();
 +        return toReturn;
 +    }
 +
 +    /**
 +     * Fetches the referenced item from the target SQLContainer.
 +     * 
 +     * @param itemId
 +     *            Item Id of the reference source (from this container)
 +     * @param refdCont
 +     *            Target SQLContainer of the reference
 +     * @return The referenced item, or null if not found
 +     */
 +    public Item getReferencedItem(Object itemId, SQLContainer refdCont) {
 +        return refdCont.getItem(getReferencedItemId(itemId, refdCont));
 +    }
 +
 +    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
 +        out.defaultWriteObject();
 +    }
 +
 +    private void readObject(java.io.ObjectInputStream in) throws IOException,
 +            ClassNotFoundException {
 +        in.defaultReadObject();
 +        if (notificationsEnabled) {
 +            /*
 +             * Register instance with CacheFlushNotifier after de-serialization
 +             * if notifications are enabled
 +             */
 +            CacheFlushNotifier.addInstance(this);
 +        }
 +    }
 +
 +    private static final Logger getLogger() {
 +        return Logger.getLogger(SQLContainer.class.getName());
 +    }
 +}
index c4b640e274fd94493e26a1131131ea0273990360,0000000000000000000000000000000000000000..6ebefcd85cee70d19434142d64d565f55767ecd4
mode 100644,000000..100644
--- /dev/null
@@@ -1,379 -1,0 +1,385 @@@
-             Object value = cp.getValue() == null ? null : cp.getValue();
-             /* Only include properties whose read-only status can be altered */
-             if (cp.isReadOnlyChangeAllowed() && !cp.isVersionColumn()) {
-                 columnToValueMap.put(cp.getPropertyId(), value);
 +/*
 + * Copyright 2011 Vaadin Ltd.
 + * 
 + * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 + * use this file except in compliance with the License. You may obtain a copy of
 + * the License at
 + * 
 + * http://www.apache.org/licenses/LICENSE-2.0
 + * 
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 + * License for the specific language governing permissions and limitations under
 + * the License.
 + */
 +package com.vaadin.data.util.sqlcontainer.query.generator;
 +
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +
 +import com.vaadin.data.Container.Filter;
 +import com.vaadin.data.util.sqlcontainer.ColumnProperty;
 +import com.vaadin.data.util.sqlcontainer.RowItem;
 +import com.vaadin.data.util.sqlcontainer.SQLUtil;
 +import com.vaadin.data.util.sqlcontainer.TemporaryRowId;
 +import com.vaadin.data.util.sqlcontainer.query.OrderBy;
 +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
 +import com.vaadin.data.util.sqlcontainer.query.generator.filter.StringDecorator;
 +
 +/**
 + * Generates generic SQL that is supported by HSQLDB, MySQL and PostgreSQL.
 + * 
 + * @author Jonatan Kronqvist / Vaadin Ltd
 + */
 +@SuppressWarnings("serial")
 +public class DefaultSQLGenerator implements SQLGenerator {
 +
 +    private Class<? extends StatementHelper> statementHelperClass = null;
 +
 +    public DefaultSQLGenerator() {
 +
 +    }
 +
 +    /**
 +     * Create a new DefaultSqlGenerator instance that uses the given
 +     * implementation of {@link StatementHelper}
 +     * 
 +     * @param statementHelper
 +     */
 +    public DefaultSQLGenerator(
 +            Class<? extends StatementHelper> statementHelperClazz) {
 +        this();
 +        statementHelperClass = statementHelperClazz;
 +    }
 +
 +    /**
 +     * Construct a DefaultSQLGenerator with the specified identifiers for start
 +     * and end of quoted strings. The identifiers may be different depending on
 +     * the database engine and it's settings.
 +     * 
 +     * @param quoteStart
 +     *            the identifier (character) denoting the start of a quoted
 +     *            string
 +     * @param quoteEnd
 +     *            the identifier (character) denoting the end of a quoted string
 +     */
 +    public DefaultSQLGenerator(String quoteStart, String quoteEnd) {
 +        QueryBuilder.setStringDecorator(new StringDecorator(quoteStart,
 +                quoteEnd));
 +    }
 +
 +    /**
 +     * Same as {@link #DefaultSQLGenerator(String, String)} but with support for
 +     * custom {@link StatementHelper} implementation.
 +     * 
 +     * @param quoteStart
 +     * @param quoteEnd
 +     * @param statementHelperClazz
 +     */
 +    public DefaultSQLGenerator(String quoteStart, String quoteEnd,
 +            Class<? extends StatementHelper> statementHelperClazz) {
 +        this(quoteStart, quoteEnd);
 +        statementHelperClass = statementHelperClazz;
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
 +     * generateSelectQuery(java.lang.String, java.util.List, java.util.List,
 +     * int, int, java.lang.String)
 +     */
 +    @Override
 +    public StatementHelper generateSelectQuery(String tableName,
 +            List<Filter> filters, List<OrderBy> orderBys, int offset,
 +            int pagelength, String toSelect) {
 +        if (tableName == null || tableName.trim().equals("")) {
 +            throw new IllegalArgumentException("Table name must be given.");
 +        }
 +        toSelect = toSelect == null ? "*" : toSelect;
 +        StatementHelper sh = getStatementHelper();
 +        StringBuffer query = new StringBuffer();
 +        query.append("SELECT " + toSelect + " FROM ").append(
 +                SQLUtil.escapeSQL(tableName));
 +        if (filters != null) {
 +            query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
 +        }
 +        if (orderBys != null) {
 +            for (OrderBy o : orderBys) {
 +                generateOrderBy(query, o, orderBys.indexOf(o) == 0);
 +            }
 +        }
 +        if (pagelength != 0) {
 +            generateLimits(query, offset, pagelength);
 +        }
 +        sh.setQueryString(query.toString());
 +        return sh;
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
 +     * generateUpdateQuery(java.lang.String,
 +     * com.vaadin.addon.sqlcontainer.RowItem)
 +     */
 +    @Override
 +    public StatementHelper generateUpdateQuery(String tableName, RowItem item) {
 +        if (tableName == null || tableName.trim().equals("")) {
 +            throw new IllegalArgumentException("Table name must be given.");
 +        }
 +        if (item == null) {
 +            throw new IllegalArgumentException("Updated item must be given.");
 +        }
 +        StatementHelper sh = getStatementHelper();
 +        StringBuffer query = new StringBuffer();
 +        query.append("UPDATE ").append(tableName).append(" SET");
 +
 +        /* Generate column<->value and rowidentifiers map */
 +        Map<String, Object> columnToValueMap = generateColumnToValueMap(item);
 +        Map<String, Object> rowIdentifiers = generateRowIdentifiers(item);
 +        /* Generate columns and values to update */
 +        boolean first = true;
 +        for (String column : columnToValueMap.keySet()) {
 +            if (first) {
 +                query.append(" " + QueryBuilder.quote(column) + " = ?");
 +            } else {
 +                query.append(", " + QueryBuilder.quote(column) + " = ?");
 +            }
 +            sh.addParameterValue(columnToValueMap.get(column), item
 +                    .getItemProperty(column).getType());
 +            first = false;
 +        }
 +        /* Generate identifiers for the row to be updated */
 +        first = true;
 +        for (String column : rowIdentifiers.keySet()) {
 +            if (first) {
 +                query.append(" WHERE " + QueryBuilder.quote(column) + " = ?");
 +            } else {
 +                query.append(" AND " + QueryBuilder.quote(column) + " = ?");
 +            }
 +            sh.addParameterValue(rowIdentifiers.get(column), item
 +                    .getItemProperty(column).getType());
 +            first = false;
 +        }
 +        sh.setQueryString(query.toString());
 +        return sh;
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
 +     * generateInsertQuery(java.lang.String,
 +     * com.vaadin.addon.sqlcontainer.RowItem)
 +     */
 +    @Override
 +    public StatementHelper generateInsertQuery(String tableName, RowItem item) {
 +        if (tableName == null || tableName.trim().equals("")) {
 +            throw new IllegalArgumentException("Table name must be given.");
 +        }
 +        if (item == null) {
 +            throw new IllegalArgumentException("New item must be given.");
 +        }
 +        if (!(item.getId() instanceof TemporaryRowId)) {
 +            throw new IllegalArgumentException(
 +                    "Cannot generate an insert query for item already in database.");
 +        }
 +        StatementHelper sh = getStatementHelper();
 +        StringBuffer query = new StringBuffer();
 +        query.append("INSERT INTO ").append(tableName).append(" (");
 +
 +        /* Generate column<->value map */
 +        Map<String, Object> columnToValueMap = generateColumnToValueMap(item);
 +        /* Generate column names for insert query */
 +        boolean first = true;
 +        for (String column : columnToValueMap.keySet()) {
 +            if (!first) {
 +                query.append(", ");
 +            }
 +            query.append(QueryBuilder.quote(column));
 +            first = false;
 +        }
 +
 +        /* Generate values for insert query */
 +        query.append(") VALUES (");
 +        first = true;
 +        for (String column : columnToValueMap.keySet()) {
 +            if (!first) {
 +                query.append(", ");
 +            }
 +            query.append("?");
 +            sh.addParameterValue(columnToValueMap.get(column), item
 +                    .getItemProperty(column).getType());
 +            first = false;
 +        }
 +        query.append(")");
 +        sh.setQueryString(query.toString());
 +        return sh;
 +    }
 +
 +    /*
 +     * (non-Javadoc)
 +     * 
 +     * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
 +     * generateDeleteQuery(java.lang.String,
 +     * com.vaadin.addon.sqlcontainer.RowItem)
 +     */
 +    @Override
 +    public StatementHelper generateDeleteQuery(String tableName,
 +            List<String> primaryKeyColumns, String versionColumn, RowItem item) {
 +        if (tableName == null || tableName.trim().equals("")) {
 +            throw new IllegalArgumentException("Table name must be given.");
 +        }
 +        if (item == null) {
 +            throw new IllegalArgumentException(
 +                    "Item to be deleted must be given.");
 +        }
 +        if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) {
 +            throw new IllegalArgumentException(
 +                    "Valid keyColumnNames must be provided.");
 +        }
 +        StatementHelper sh = getStatementHelper();
 +        StringBuffer query = new StringBuffer();
 +        query.append("DELETE FROM ").append(tableName).append(" WHERE ");
 +        int count = 1;
 +        for (String keyColName : primaryKeyColumns) {
 +            if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
 +                    && keyColName.equalsIgnoreCase("rownum")) {
 +                count++;
 +                continue;
 +            }
 +            if (count > 1) {
 +                query.append(" AND ");
 +            }
 +            if (item.getItemProperty(keyColName).getValue() != null) {
 +                query.append(QueryBuilder.quote(keyColName) + " = ?");
 +                sh.addParameterValue(item.getItemProperty(keyColName)
 +                        .getValue(), item.getItemProperty(keyColName).getType());
 +            }
 +            count++;
 +        }
 +        if (versionColumn != null) {
 +            query.append(String.format(" AND %s = ?",
 +                    QueryBuilder.quote(versionColumn)));
 +            sh.addParameterValue(
 +                    item.getItemProperty(versionColumn).getValue(), item
 +                            .getItemProperty(versionColumn).getType());
 +        }
 +
 +        sh.setQueryString(query.toString());
 +        return sh;
 +    }
 +
 +    /**
 +     * Generates sorting rules as an ORDER BY -clause
 +     * 
 +     * @param sb
 +     *            StringBuffer to which the clause is appended.
 +     * @param o
 +     *            OrderBy object to be added into the sb.
 +     * @param firstOrderBy
 +     *            If true, this is the first OrderBy.
 +     * @return
 +     */
 +    protected StringBuffer generateOrderBy(StringBuffer sb, OrderBy o,
 +            boolean firstOrderBy) {
 +        if (firstOrderBy) {
 +            sb.append(" ORDER BY ");
 +        } else {
 +            sb.append(", ");
 +        }
 +        sb.append(QueryBuilder.quote(o.getColumn()));
 +        if (o.isAscending()) {
 +            sb.append(" ASC");
 +        } else {
 +            sb.append(" DESC");
 +        }
 +        return sb;
 +    }
 +
 +    /**
 +     * Generates the LIMIT and OFFSET clause.
 +     * 
 +     * @param sb
 +     *            StringBuffer to which the clause is appended.
 +     * @param offset
 +     *            Value for offset.
 +     * @param pagelength
 +     *            Value for pagelength.
 +     * @return StringBuffer with LIMIT and OFFSET clause added.
 +     */
 +    protected StringBuffer generateLimits(StringBuffer sb, int offset,
 +            int pagelength) {
 +        sb.append(" LIMIT ").append(pagelength).append(" OFFSET ")
 +                .append(offset);
 +        return sb;
 +    }
 +
 +    protected Map<String, Object> generateColumnToValueMap(RowItem item) {
 +        Map<String, Object> columnToValueMap = new HashMap<String, Object>();
 +        for (Object id : item.getItemPropertyIds()) {
 +            ColumnProperty cp = (ColumnProperty) item.getItemProperty(id);
 +            /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */
 +            if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
 +                    && cp.getPropertyId().equalsIgnoreCase("rownum")) {
 +                continue;
 +            }
-             Object value = cp.getValue() == null ? null : cp.getValue();
-             if (!cp.isReadOnlyChangeAllowed() || cp.isVersionColumn()) {
++            if (cp.isPersistent()) {
++                columnToValueMap.put(cp.getPropertyId(), cp.getValue());
 +            }
 +        }
 +        return columnToValueMap;
 +    }
 +
 +    protected Map<String, Object> generateRowIdentifiers(RowItem item) {
 +        Map<String, Object> rowIdentifiers = new HashMap<String, Object>();
 +        for (Object id : item.getItemPropertyIds()) {
 +            ColumnProperty cp = (ColumnProperty) item.getItemProperty(id);
 +            /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */
 +            if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
 +                    && cp.getPropertyId().equalsIgnoreCase("rownum")) {
 +                continue;
 +            }
++
++            if (cp.isRowIdentifier()) {
++                Object value;
++                if (cp.isPrimaryKey()) {
++                    // If the value of a primary key has changed, its old value
++                    // should be used to identify the row (#9145)
++                    value = cp.getOldValue();
++                } else {
++                    value = cp.getValue();
++                }
 +                rowIdentifiers.put(cp.getPropertyId(), value);
 +            }
 +        }
 +        return rowIdentifiers;
 +    }
 +
 +    /**
 +     * Returns the statement helper for the generator. Override this to handle
 +     * platform specific data types.
 +     * 
 +     * @see http://dev.vaadin.com/ticket/9148
 +     * @return a new instance of the statement helper
 +     */
 +    protected StatementHelper getStatementHelper() {
 +        if (statementHelperClass == null) {
 +            return new StatementHelper();
 +        }
 +
 +        try {
 +            return statementHelperClass.newInstance();
 +        } catch (InstantiationException e) {
 +            throw new RuntimeException(
 +                    "Unable to instantiate custom StatementHelper", e);
 +        } catch (IllegalAccessException e) {
 +            throw new RuntimeException(
 +                    "Unable to instantiate custom StatementHelper", e);
 +        }
 +    }
 +
 +}
index 0000000000000000000000000000000000000000,7460e15af019d7360ce09fa5c647b80c8392cc15..1bb3e7abe26026836d1d1d85256139ee755661db
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,42 +1,43 @@@
 -        applet.getWindow().addWindow(window);
+ package com.vaadin.tests.components.embedded;
+ import com.vaadin.terminal.ExternalResource;
+ import com.vaadin.tests.components.TestBase;
+ import com.vaadin.ui.Button;
+ import com.vaadin.ui.Button.ClickEvent;
+ import com.vaadin.ui.Embedded;
+ import com.vaadin.ui.Label;
+ import com.vaadin.ui.Window;
+ public class EmbeddedApplet extends TestBase {
+     @Override
+     protected String getDescription() {
+         return "The sub window should be shown on top of the embedded applet";
+     }
+     @Override
+     protected Integer getTicketNumber() {
+         return 8399;
+     }
+     @Override
+     public void setup() {
+         final Embedded applet = new Embedded();
+         applet.setType(Embedded.TYPE_BROWSER);
+         applet.setWidth("400px");
+         applet.setHeight("300px");
+         applet.setSource(new ExternalResource("/statictestfiles/applet.html"));
+         addComponent(applet);
+         addComponent(new Button("Remove applet", new Button.ClickListener() {
++            @Override
+             public void buttonClick(ClickEvent event) {
+                 removeComponent(applet);
+             }
+         }));
+         Window window = new Window("Testwindow");
+         window.addComponent(new Label("I am inside the window"));
++        applet.getRoot().addWindow(window);
+     }
+ }
index 0000000000000000000000000000000000000000,d252b89aa343ba64e944d3e7ea7beffd751a4d4e..8fefac16a19ed3d9b9fc74523be7adc41b0ce507
mode 000000,100755..100755
--- /dev/null
@@@ -1,0 -1,34 +1,31 @@@
 -import com.vaadin.ui.Window.Notification;
+ package com.vaadin.tests.components.notification;
+ import com.vaadin.tests.components.TestBase;
+ import com.vaadin.tests.util.LoremIpsum;
+ import com.vaadin.ui.Label;
 -        getMainWindow()
 -                .showNotification(
 -                        new Notification(
 -                                "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;This should be a <br/><b>SEMI-TRANSPARENT</b><br/> notification",
 -                                Notification.TYPE_WARNING_MESSAGE));
++import com.vaadin.ui.Notification;
+ public class SemiTransparentNotification extends TestBase {
+     @Override
+     protected void setup() {
+         Label l = new Label(LoremIpsum.get(10000));
+         getLayout().setSizeFull();
+         addComponent(l);
++        Notification.show("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;This should be a <br/><b>SEMI-TRANSPARENT</b><br/> notification",
++                        Notification.TYPE_WARNING_MESSAGE);
+     }
+     @Override
+     protected String getDescription() {
+         // TODO Auto-generated method stub
+         return null;
+     }
+     @Override
+     protected Integer getTicketNumber() {
+         // TODO Auto-generated method stub
+         return null;
+     }
+ }
index 6461d10277b13765dbf9570587a2bb81f27ade57,baca96d3c3e5ae61b882c12b4456520e01dba4f3..ba4fe93b681265b25bec195aa69fb231b0f00995
@@@ -46,17 -46,15 +46,17 @@@ public class PopupViewClickShortcut ext
          l.setWidth(null);
  
          Button b = new Button("Submit " + caption + " (Ctrl+Alt+"
-                 + String.valueOf(Character.toChars(keyCode)) + ")",
-                 new Button.ClickListener() {
-                     private int i = 5;
+                 + (char) keyCode + ")", new Button.ClickListener() {
+             private int i = 5;
++            @Override
+             public void buttonClick(ClickEvent event) {
+                 log.log("Submitted from "
+                         + event.getButton().getParent().getCaption());
+                 t.addItem(new String[] { "added " + i++ }, i);
+             }
+         });
 +
-                     @Override
-                     public void buttonClick(ClickEvent event) {
-                         log.log("Submitted from "
-                                 + event.getButton().getParent().getCaption());
-                         t.addItem(new String[] { "added " + i++ }, i);
-                     }
-                 });
          b.setClickShortcut(keyCode, ModifierKey.CTRL, ModifierKey.ALT);
  
          l.addComponent(t);