--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>Vaadin Framework @version@</title>
+<link rel="stylesheet" type="text/css" href="css/styles.css" />
+
+<!--[if lte IE 6]>
+ <link rel="stylesheet" type="text/css" href="css/ie.css" />
+ <![endif]-->
+<style type="text/css">
+.nested-list ol {
+ counter-reset: item
+}
+
+.nested-list li {
+ display: block
+}
+
+.nested-list li:before {
+ content: counters(item, ".") ". ";
+ counter-increment: item
+}
+</style>
+</head>
+
+<body>
+ <div id="header">
+ <h1>Vaadin – thinking of U and I</h1>
+ <div id="version">
+ <strong>Version @version@</strong>
+ </div>
+ </div>
+ <!-- /header -->
+
+ <div id="content">
+ <p>Version @version@ built on @builddate@.</p>
+
+ <h2 id="tableofcontents">Release Notes for Vaadin Framework
+ @version@</h2>
+ <ul>
+ <li><a href="#overview">Overview of Vaadin
+ @version@ Release</a></li>
+ <li><a href="#changelog">Change Log for Vaadin
+ @version@</a></li>
+ <li><a href="#enhancements">Enhancements in Vaadin
+ @version-minor@</a></li>
+ <li><a href="#incompatible">Incompatible or Behavior-altering Changes in
+ @version-minor@</a></li>
+ <li><a href="#knownissues">Known Issues and Limitations</a></li>
+ <li><a href="#vaadin">Vaadin Installation</a></li>
+ <li><a href="#package">Package Contents</a></li>
+ <li><a href="#migrating">Migrating from Vaadin 6</a></li>
+ <li><a href="#dependencies">Vaadin @version@
+ dependencies</a></li>
+ <li><a href="#upgrading">Upgrading to Vaadin
+ @version-minor@</a></li>
+ <li><a href="#supportedversions">Supported
+ Technologies</a></li>
+ <li><a href="#vaadinontheweb">Vaadin on the Web</a></li>
+ </ul>
+
+ <h2 id="overview">Overview of Vaadin @version@ Release</h2>
+
+ <p>
+ Vaadin @version@ is a
+<!-- feature release that includes -->
+ pre-release for evaluating
+ a number of new features and bug fixes, as listed in the <a
+ href="#enhancements">list of enhancements</a> and <a
+ href="#changelog">change log</a> below.
+ </p>
+
+ <!-- ================================================================ -->
+ <h3 id="changelog">Change Log for Vaadin @version@</h3>
+
+ <p>This release includes the following closed issues:</p>
+
+ <table>
+ @release-notes-tickets@
+ <tr><td> </td><td></td></tr>
+ <tr><td class="fv"><span class="vote">Vote</span></td><td colspan="2" class="pad">Enhancements <a href=" https://vaadin.com/support">Vaadin support</a> users have voted for</td></tr>
+ <tr><td class="bfp"><span class="bfp">Priority</span></td><td colspan="2" class="pad">Defects <a href=" https://vaadin.com/support">Vaadin support</a> users have prioritized</td></tr>
+ </table>
+ <br/>
+ <p>
+ You can also view the <a
+ href="http://dev.vaadin.com/query?status=pending-release&status=released&resolution=fixed&milestone=Vaadin+@version@&order=id">list
+ of the closed issues</a> at the Vaadin developer's site.
+ </p>
+
+ <h2 id="enhancements">Enhancements in Vaadin @version-minor@</h2>
+
+ <p>Vaadin @version-minor@ includes many major and minor
+ enhancements. Below is a list of the most notable changes:</p>
+
+ <ul>
+ <li>New configurable client-server automatic reconnect handling and warning dialogs</li>
+ <li>Grid columns can be resized by the user</li>
+ <li>Grid non-modal editor mode and unbuffered datasource binding</li>
+ <li>More fluent Grid scrolling for touch devices</li>
+ <li>Better customizability of Grid (data generators for communication, customizable selection models etc.)</li>
+ <li>Configurable keyboard, touch and mouse navigation support for Grid editor</li>
+ <li>Declarative HTML serialization enhancements for Vaadin Designer 1.0</li>
+ <li>Better handling of closing a window with keyboard shortcuts</li>
+ <li>Column collapsing events for Table</li>
+ <li>Most Components fire a ContextClickEvent when the user tries to open a context menu.
+ This event is specialised to contain some extra context for Grid, Table, TreeTable and Tree,
+ and used by the <a href="https://vaadin.com/addon/vaadin-contextmenu">ContextMenu add-on</a>.</li>
+ <li>New push transport mode WEBSOCKET_XHR (WebSockets from server to client, XHR from client to
+ server) which allows setting of cookies, using request scopes with CDI/Spring etc.</li>
+ <li>Navigator is more extensible and can be used with dependency injection</li>
+ <li>It is possible to customize the push URL using PushConfiguration.setPushUrl().
+ This enables implementing some push related add-ons such as push on portals.</li>
+ </ul>
+
+ <p>
+ For enhancements introduced in Vaadin 7.5, see the <a
+ href="http://vaadin.com/download/release/7.5/7.5.0/release-notes.html">Release
+ Notes for Vaadin 7.5.0</a>.
+ </p>
+
+ <h3 id="incompatible">Incompatible or Behavior-altering Changes in @version-minor@</h3>
+ <ul>
+ <li>Window.setCloseShortcut() is now deprecated. ESCAPE is no longer a hard-coded default, but rather a soft one, and
+ can be removed. If the close-shortcut attribute of the v-windowelement is present, it must list all close
+ shortcuts, including ESCAPE, separated by whitespace. Existing, unchanged code should behave as before.
+ See ticket <a href="https://dev.vaadin.com/ticket/17383">#17383</a> for more information on the reasoning
+ behind the change.</li>
+ <li>Grid SelectionModels are now Extensions. This update removes all selection related variables and API from
+ GridConnector, GridState, GridServerRpc and GridClientRpc</li>
+ <li>StringToEnumConverter now explicitly supports Enum types with custom toString() implementations.
+ This may affect applications that relied on the undefined behavior in previous versions.</li>
+ <li>The Section enumeration from client-side of the Grid has been moved to GridConstants</li>
+ <li>The order in which AttachEvents are fired has been changed. When attaching a component with child components,
+ events are now fired first for the children and last for the root component. The order is thus the same as
+ the order in which custom code in overridden Component.attach methods would be executed. Please refer to
+ ticket <a href="https://dev.vaadin.com/ticket/16348">#16348</a> for more information on this change.</li>
+ <li>Jsoup library version 1.8.3 is incompatible with the version 1.8.1 used in early 7.6 alphas and 7.5. This might
+ cause problems with external libraries compiled against said versions.</li>
+ <li>Declarative format is now using "vaadin-" as a default prefix instead of the "v-" prefix used in 7.5.
+ This default can be changed in deployment configuration.</li>
++ <li>The annotations @PreserveOnRefresh, @Push, @Theme, @Title, @VaadinServletConfiguration and @Widgetset now use
++ @Inherited. The annotation is also looked up in extended interfaces for backwards compatibility.</li>
++ <li>Server-side timings of request processing are only sent to the client when not in production mode. Using the
++ timings in TestBench tests requires the server not to be in production mode.</li>
+ </ul>
+ <h3 id="knownissues">Known Issues and Limitations</h3>
+ <ul>
+ <li>Vaadin TouchKit versions prior to 4.1.0 do not work with 7.6. Please use version 4.1.0 or above.</li>
+ <li>Context click events are not generated on iOS devices
+ (<a href="http://dev.vaadin.com/ticket/19367">#19367</a>)</li>
+ <li>Drag'n'drop in a Table doesn't work on touch devices running
+ Internet Explorer (Windows Phone, Surface)
+ (<a href="http://dev.vaadin.com/ticket/13737">#13737</a>)
+ </li>
+ <li><p>It is currently not possible to specify <tt>font-size</tt>
+ as <tt>em</tt> or <tt>%</tt>, or layout component sizes
+ with <tt>em</tt> (<a
+ href="http://dev.vaadin.com/ticket/10634">#10634</a>).</p><p>This
+ does not apply to Valo, but using em sizes to size layouts is discouraged,
+ because it results in fractional component sizes in many cases, which
+ might cause unwanted 1px gaps between components.</p>
+ </li>
+ <li>Up-to-date information about configuration needed for push and compatibility issues are available in
+ <a href="https://vaadin.com/wiki/-/wiki/Main/Configuring+push+for+your+environment">this wiki page.</a>
+ <br />
+ The most prominent limitations are:
+ <ul>
+ <li>Push is currently not supported in portals (See <a
+ href="http://dev.vaadin.com/ticket/11493">#11493</a>)
+ </li>
+ <li>>Transport mode WEBSOCKET does not support invalidating the session on Tomcat 7,
+ nor setting cookies for the session. The new transport mode WEBSOCKET_XHR can be
+ used to bypass these limitations.
+ <br />
+ See tickets <a href="https://dev.vaadin.com/ticket/11808">#11808</a> and
+ <a href="https://dev.vaadin.com/ticket/11721">#11721</a>
+ </li>
+ </ul>
+ </li>
+ <li>Google App Engine has some limitations. Please consult Vaadin Wiki:
+ <a href="https://vaadin.com/wiki/-/wiki/Main/Vaadin+support+for+Google+App+Engine">Vaadin support for Google App Engine</a>
+ </li>
+ <li>Grid does not support adding components to cells. Instead light-weight Renderers can be
+ used to present and edit data.</li>
+ </ul>
+
+ <h2 id="vaadin">Vaadin Installation</h2>
+
+ <p>
+ <b>Vaadin</b> is a Java framework for building modern web
+ applications that look great, perform well and make you and
+ your users happy. <b>Vaadin</b> is available under the
+ Apache License, Version 2.0 (see the
+ <tt>license.html</tt>
+ in the Vaadin ZIP or JAR package).
+ </p>
+
+ <p>
+ The easiest ways to install <b>Vaadin</b> are:
+ </p>
+
+ <ul>
+ <li><strong>Maven dependency.</strong>If using Maven, define it as a dependency or use
+ any of the available archetypes to create a new project. More information at
+ <a href="https://vaadin.com/maven">https://vaadin.com/maven</a>
+ </li>
+
+ <li><strong>IDE Plugins.</strong>If using Eclipse, use the
+ <a href="http://vaadin.com/eclipse">Vaadin Plugin for Eclipse</a>, which automatically
+ downloads the Vaadin libraries. For NetBeans 8.0 and 7.4, use the
+ <a href="http://plugins.netbeans.org/plugin/50531/vaadin-plug-in-for-netbeans">official Vaadin plugin</a>
+ that provides Maven based wizards and code completion support for Vaadin development.
+ </li>
+ </ul>
+
+ <p>
+ Vaadin Framework is also available as a ZIP package downloadable from <a
+ href="http://vaadin.com/download">Vaadin Download
+ page</a>.
+ </p>
+
+ <h3 id="package">Package Contents</h3>
+
+ <p>Inside the ZIP installation package you will find:</p>
+
+ <ul>
+ <li>Separate server-side (<tt>vaadin-server</tt>) and
+ client-side (<tt>vaadin-client</tt>, <tt>vaadin-client-compiler</tt>)
+ development libraries
+ </li>
+ <li>Precompiled widget set (<tt>vaadin-client-compiled</tt>)
+ for server-side development
+ </li>
+ <li>Shared library (<tt>vaadin-shared</tt>) for both
+ server- and client-side libraries
+ </li>
+ <li>Built-in themes (<tt>vaadin-themes</tt>)
+ </li>
+ <li>Dependency libraries provided under the <tt>lib/</tt>
+ folder
+ </li>
+ </ul>
+
+ <p>
+ See the
+ <tt>README.TXT</tt>
+ in the installation package for detailed information about
+ the package contents. <a href="http://vaadin.com/book">Book
+ of Vaadin</a> (for Vaadin 7) gives more detailed
+ instructions.
+ </p>
+
+ <p>
+ For server-side development, copy the
+ <tt>vaadin-server</tt>
+ ,
+ <tt>vaadin-client-compiled</tt>
+ ,
+ <tt>vaadin-shared</tt>
+ , and
+ <tt>vaadin-themes</tt>
+ from the main folder and the dependencies from the
+ <tt>lib</tt>
+ folder to the
+ <tt>WEB-INF/lib</tt>
+ folder of your Vaadin project. (The
+ <tt>vaadin-client-compiled</tt>
+ is necessary if you do not wish to compile the widget set by
+ your own, which you need to do if you use almost any add-on
+ components.)
+ </p>
+
+ <h4 id="package.updates">Updates to the Packaging</h4>
+ <p>
+ Since Vaadin 7.2.0, the old vaadin-theme-compiler has been moved into
+ a separate project and renamed to vaadin-sass-compiler. It is now included
+ along with the other 3rd party dependencies in the ZIP package.
+ </p>
+
+ <p>
+ For pure client-side development, you only need the
+ <tt>vaadin-client</tt>
+ and
+ <tt>vaadin-client-compiler</tt>
+ JARs, which should be put to a non-deployed project library
+ folder, such as
+ <tt>lib</tt>
+ . You also need them if you compile the widget set for any
+ reason, such as using Vaadin add-ons, or create new
+ server-side components integrated with client-side widgets.
+ </p>
+
+ <h2 id="migrating">Migrating from Vaadin 6</h2>
+
+ <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>UI</b>
+ or <b>UI.LegacyApplication</b> instead of <b>Application</b>.
+ A detailed list of migration changes are given in the <a
+ href="https://vaadin.com/wiki/-/wiki/Main/Migrating+from+Vaadin+6+to+Vaadin+7">Vaadin
+ 7 Migration Guide</a>.
+ </p>
+
+ <p>Any custom client-side widgets need to be ported to use
+ the new client-server communication API, or the Vaadin 6
+ compatibility API.</p>
+
+ <p>
+ 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>
+
+ <h2 id="dependencies">Vaadin @version@ Dependencies</h2>
+
+ <p>When using Maven, Ivy, Gradle, or other dependency
+ management system, all Vaadin dependencies are downloaded
+ automatically. This is also the case when using the Vaadin
+ Plugin for Eclipse.</p>
+
+ <p>
+ The Vaadin ZIP installation package includes the
+ dependencies in the
+ <tt>lib</tt>
+ subfolder. These need to be copied to the
+ <tt>WEB-INF/lib</tt>
+ folder of the web application that uses Vaadin.
+ </p>
+
+ <p>
+ The dependencies are listed in the <a href="license.html">Licensing
+ description</a>. Some are explicit dependencies packaged and
+ distributed as separate JARs, while some are included inside
+ other libraries.
+ </p>
+
+ <h3>Bean Validation</h3>
+
+ <p>
+ If you use the bean validation feature in Vaadin 7, you need
+ a Bean Validation API implementation. You need to install
+ the implementation JAR in the
+ <tt>WEB-INF/lib</tt>
+ directory of the web application that uses validation.
+ </p>
+
+ <h2 id="upgrading">Upgrading to Vaadin @version-minor@</h2>
+
+ <p>When upgrading from an earlier Vaadin version, you must:
+ </p>
+
+ <ul>
+ <li>Recompile your classes using the new Vaadin
+ version. Binary compatibility is only guaranteed for
+ maintenance releases of Vaadin.</li>
+
+ <li>Unless using the precompiled widget set, recompile
+ your widget set using the new Vaadin version.</li>
+ </ul>
+
+ <p>Remember also to refresh the project in your IDE to
+ ensure that the new version of everything is in use.</p>
+
+ <p>
+ By using the "
+ <tt>?debug</tt>
+ " URL parameter, you can verify that the version of the
+ servlet, the theme, and the widget set all match.
+ </p>
+
+ <p>
+ <b>Eclipse</b> users should always check if there is a new
+ version of the Eclipse Plug-in available. The Eclipse
+ Plug-in can be used to update the Vaadin version in the
+ project (Project properties » Vaadin).
+ </p>
+
+ <p>
+ <b>Maven</b> users should update the Vaadin dependency
+ version in the
+ <tt>pom.xml</tt>
+ unless it is defined as
+ <tt>LATEST</tt>
+ . You must also ensure that the GWT dependency uses the
+ correct version and recompile your project and your widget
+ set.
+ </p>
+
+ <p>
+ <b>Liferay and other portal</b> users must install the
+ Vaadin libraries in
+ <t>ROOT/WEB-INF/lib/</b> in the portal (and remove a
+ possibly obsolete older <tt>vaadin.jar</tt>). Additionally,
+ the contents of the <tt>vaadin-client-compiled</tt> and <tt>vaadin-themes</tt>
+ must be extracted to the <tt>ROOT/html/VAADIN</tt> directory
+ in the Liferay installation. If your portal uses custom
+ widgets, you can use <a
+ href="http://vaadin.com/directory#addon/liferay-control-panel-plugin-for-vaadin:vaadin">
+ Liferay Control Panel for Vaadin</a> for easy widget set compilation.</t>
+ </p>
+
+ <h2 id="supportedversions">Supported Technologies</h2>
+
+ <p>
+ Vaadin 7 is compatible with <b>Java 6</b> and newer. Vaadin
+ 7 is especially supported on the following <b>operating
+ systems</b>:
+ </p>
+
+ <ul>
+ <li>Windows</li>
+ <li>Linux</li>
+ <li>Mac OS X</li>
+ </ul>
+
+ <p>
+ Vaadin 7 requires <b>Java Servlet API 2.4</b> but also
+ supports later versions and should work with any Java
+ application server that conforms to the standard. The
+ following <b>application servers</b> are supported:
+ </p>
+
+ <ul>
+ <li>Apache Tomcat 5-8</li>
+ <li>Apache TomEE 1</li>
+ <li>Oracle WebLogic Server 10.3-12</li>
+ <li>IBM WebSphere Application Server 7-8</li>
+ <li>JBoss Application Server 4-7</li>
+ <li>Wildfly 8-9</li>
+ <li>Jetty 5-9</li>
+ <li>Glassfish 2-4</li>
+ </ul>
+
+ <p>
+ Vaadin 7 supports the JSR-286 Portlet specification and all
+ portals that implement the specification should work. The
+ following <b>portals</b> are supported:
+ </p>
+
+ <ul>
+ <li>Liferay Portal 5.2-6</li>
+ <li>GateIn Portal 3</li>
+ <li>eXo Platform 3</li>
+ <li>IBM WebSphere Portal 8</li>
+ </ul>
+
+ <p>
+ Vaadin also supports <b>Google App Engine</b>.
+ </p>
+
+ <p>
+ Vaadin @version@ supports the following <b>desktop browsers</b>:
+ </p>
+
+ <ul>
+ <li>Mozilla Firefox 18-43</li>
+ <li>Mozilla Firefox 17 ESR, 24 ESR, 31 ESR, 38 ESR</li>
+ <li>Internet Explorer 8-11, Edge</li>
+ <li>Safari 6-8</li>
+ <li>Opera 16-34</li>
+ <li>Google Chrome 23-47</li>
+ </ul>
+
+ <p>
+ Additionally, Vaadin supports the built-in browsers in the
+ following <b>mobile operating systems</b>:
+ </p>
+
+ <ul>
+ <li>iOS 5-9</li>
+ <li>Android 2.3-5</li>
+ <li>Windows Phone 8</li>
+ </ul>
+
+ <p>Vaadin SQL Container supports the following databases:</p>
+ <ul>
+ <li>HSQLDB</li>
+ <li>MySQL</li>
+ <li>MSSQL</li>
+ <li>Oracle</li>
+ <li>PostgreSQL</li>
+ </ul>
+
+ <h2 id="vaadinontheweb">Vaadin on the Web</h2>
+
+ <ul>
+ <li><a href="http://vaadin.com">vaadin.com - The
+ developer portal containing everything you need to
+ know about Vaadin</a></li>
+ <li><a href="http://vaadin.com/demo">vaadin.com/demo
+ - A collection of demos for Vaadin</a></li>
+ <li><a href="http://vaadin.com/learn">vaadin.com/learn
+ - Getting started with Vaadin</a></li>
+ <li><a href="http://vaadin.com/forum">vaadin.com/forum
+ - Forums for Vaadin related discussions</a></li>
+ <li><a href="http://vaadin.com/book">vaadin.com/book
+ - Book of Vaadin - everything you need to know about
+ Vaadin</a></li>
+ <li><a href="http://vaadin.com/api">vaadin.com/api
+ - Online javadocs</a></li>
+ <li><a href="http://vaadin.com/directory">vaadin.com/directory
+ - Add-ons for Vaadin</a></li>
+
+ <li><a href="http://vaadin.com/pro-tools">vaadin.com/pro-tools
+ - Commercial tools for Vaadin development</a></li>
+ <li><a href="http://vaadin.com/support">vaadin.com/support
+ - Commercial support for Vaadin development </a></li>
+ <li><a href="http://vaadin.com/services">vaadin.com/services
+ - Expert services for Vaadin</a></li>
+ <li><a href="http://vaadin.com/company">vaadin.com/company
+ - Information about the company behind Vaadin</a></li>
+
+ <li><a href="http://dev.vaadin.com">dev.vaadin.com
+ - Bug tracker</a></li>
+
+ <li><a
+ href="http://dev.vaadin.com/wiki/Vaadin/Development/StartingVaadin7Development">How
+ to get the source code of Vaadin</a></li>
+ </ul>
+
+ </div>
+ <!-- /content-->
+
+ <div id="footer">
+ <span class="slogan"><strong>vaadin <em>}></em>
+ </strong> thinking of U and I<span> <a href="#top">↑ Back
+ to top</a>
+ </div>
+ <!-- /footer -->
+</body>
+</html>
+
+<!-- Keep this comment at the end of the file
+Local variables:
+mode: xml
+sgml-omittag:nil
+sgml-shorttag:nil
+sgml-namecase-general:nil
+sgml-general-insert-case:lower
+sgml-minimize-attributes:nil
+sgml-always-quote-attributes:t
+sgml-indent-step:2
+sgml-indent-data:t
+sgml-parent-document:nil
+sgml-exposed-tags:nil
+sgml-local-catalogs:("/etc/sgml/catalog" "/usr/share/xemacs21/xemacs-packages/etc/psgml-dtds/CATALOG")
+sgml-local-ecat-files:("ECAT" "~/sgml/ECAT" "/usr/share/sgml/ECAT" "/usr/local/share/sgml/ECAT" "/usr/local/lib/sgml/ECAT")
+End:
+-->
--- /dev/null
- bundle.setNeedsSerialize(presentationType);
+/*
+ * Copyright 2000-2014 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.server.widgetsetutils.metadata;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JParameterizedType;
+import com.google.gwt.core.ext.typeinfo.JType;
++import com.google.gwt.core.ext.typeinfo.NotFoundException;
+import com.vaadin.client.connectors.AbstractRendererConnector;
+
++import elemental.json.JsonValue;
++
+/**
+ * Generates type data for renderer connectors.
+ * <ul>
+ * <li>Stores the return type of the overridden
+ * {@link AbstractRendererConnector#getRenderer() getRenderer} method to enable
+ * automatic creation of an instance of the proper renderer type.
+ * <li>Stores the presentation type of the connector to enable the
+ * {@link AbstractRendererConnector#decode(elemental.json.JsonValue) decode}
+ * method to work without having to implement a "getPresentationType" method.
+ * </ul>
+ *
+ * @see WidgetInitVisitor
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class RendererVisitor extends TypeVisitor {
+
+ @Override
+ public void visitConnector(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) throws UnableToCompleteException {
+ if (ConnectorBundle.isConnectedRendererConnector(type)) {
+ doRendererType(logger, type, bundle);
+ doPresentationType(logger, type, bundle);
+ }
+ }
+
+ private static void doRendererType(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) throws UnableToCompleteException {
+ // The class in which createRenderer is implemented
+ JClassType createRendererClass = ConnectorBundle.findInheritedMethod(
+ type, "createRenderer").getEnclosingType();
+
+ // Needs GWT constructor if createRenderer is not overridden
+ if (createRendererClass.getQualifiedSourceName().equals(
+ AbstractRendererConnector.class.getCanonicalName())) {
+
+ JMethod getRenderer = ConnectorBundle.findInheritedMethod(type,
+ "getRenderer");
+ if (getRenderer.getEnclosingType().getQualifiedSourceName()
+ .equals(AbstractRendererConnector.class.getCanonicalName())) {
+ logger.log(Type.ERROR, type.getQualifiedSourceName()
+ + " must override either createRenderer or getRenderer");
+ throw new UnableToCompleteException();
+ }
+ JClassType rendererType = getRenderer.getReturnType().isClass();
+
+ bundle.setNeedsGwtConstructor(rendererType);
+
+ // Also needs renderer type to find the right GWT constructor
+ bundle.setNeedsReturnType(type, getRenderer);
+
+ logger.log(Type.DEBUG, "Renderer type of " + type + " is "
+ + rendererType);
+ }
+ }
+
+ private void doPresentationType(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) throws UnableToCompleteException {
+ JType presentationType = getPresentationType(type, logger);
+ bundle.setPresentationType(type, presentationType);
+
++ if (!hasCustomDecodeMethod(type, logger)) {
++ bundle.setNeedsSerialize(presentationType);
++ }
+
+ logger.log(Type.DEBUG, "Presentation type of " + type + " is "
+ + presentationType);
+ }
+
++ private static boolean hasCustomDecodeMethod(JClassType type,
++ TreeLogger logger) throws UnableToCompleteException {
++ try {
++ JMethod decodeMethod = ConnectorBundle.findInheritedMethod(type,
++ "decode",
++ type.getOracle().getType(JsonValue.class.getName()));
++ if (decodeMethod == null) {
++ throw new NotFoundException();
++ }
++
++ return !decodeMethod.getEnclosingType().getQualifiedSourceName()
++ .equals(AbstractRendererConnector.class.getName());
++ } catch (NotFoundException e) {
++ logger.log(Type.ERROR, "Can't find decode method for renderer "
++ + type, e);
++ throw new UnableToCompleteException();
++ }
++ }
++
+ private static JType getPresentationType(JClassType type, TreeLogger logger)
+ throws UnableToCompleteException {
+ JClassType originalType = type;
+ while (type != null) {
+ if (type.getQualifiedBinaryName().equals(
+ AbstractRendererConnector.class.getName())) {
+ JParameterizedType parameterized = type.isParameterized();
+ if (parameterized == null) {
+ logger.log(
+ Type.ERROR,
+ type.getQualifiedSourceName()
+ + " must define the generic parameter of the inherited "
+ + AbstractRendererConnector.class
+ .getSimpleName());
+ throw new UnableToCompleteException();
+ }
+ return parameterized.getTypeArgs()[0];
+ }
+ type = type.getSuperclass();
+ }
+ throw new IllegalArgumentException("The type "
+ + originalType.getQualifiedSourceName() + " does not extend "
+ + AbstractRendererConnector.class.getName());
+ }
+}
--- /dev/null
- import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent;
+/*
+ * Copyright 2000-2014 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.client;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.google.gwt.aria.client.LiveValue;
+import com.google.gwt.aria.client.RelevantValue;
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.HasHandlers;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ApplicationConfiguration.ErrorMessage;
- var smh = ap.@com.vaadin.client.ApplicationConnection::getMessageHandler();
+import com.vaadin.client.ResourceLoader.ResourceLoadEvent;
+import com.vaadin.client.ResourceLoader.ResourceLoadListener;
+import com.vaadin.client.communication.ConnectionStateHandler;
+import com.vaadin.client.communication.Heartbeat;
+import com.vaadin.client.communication.MessageHandler;
+import com.vaadin.client.communication.MessageSender;
+import com.vaadin.client.communication.RpcManager;
+import com.vaadin.client.communication.ServerRpcQueue;
+import com.vaadin.client.componentlocator.ComponentLocator;
+import com.vaadin.client.metadata.ConnectorBundleLoader;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.client.ui.FontIcon;
+import com.vaadin.client.ui.Icon;
+import com.vaadin.client.ui.ImageIcon;
+import com.vaadin.client.ui.VContextMenu;
+import com.vaadin.client.ui.VNotification;
+import com.vaadin.client.ui.VOverlay;
+import com.vaadin.client.ui.ui.UIConnector;
+import com.vaadin.shared.VaadinUriResolver;
+import com.vaadin.shared.Version;
+import com.vaadin.shared.communication.LegacyChangeVariablesInvocation;
+import com.vaadin.shared.util.SharedUtil;
+
+/**
+ * This is the client side communication "engine", managing client-server
+ * communication with its server side counterpart
+ * com.vaadin.server.VaadinService.
+ *
+ * Client-side connectors receive updates from the corresponding server-side
+ * connector (typically component) as state updates or RPC calls. The connector
+ * has the possibility to communicate back with its server side counter part
+ * through RPC calls.
+ *
+ * TODO document better
+ *
+ * Entry point classes (widgetsets) define <code>onModuleLoad()</code>.
+ */
+public class ApplicationConnection implements HasHandlers {
+
+ @Deprecated
+ public static final String MODIFIED_CLASSNAME = StyleConstants.MODIFIED;
+
+ @Deprecated
+ public static final String DISABLED_CLASSNAME = StyleConstants.DISABLED;
+
+ @Deprecated
+ public static final String REQUIRED_CLASSNAME = StyleConstants.REQUIRED;
+
+ @Deprecated
+ public static final String REQUIRED_CLASSNAME_EXT = StyleConstants.REQUIRED_EXT;
+
+ @Deprecated
+ public static final String ERROR_CLASSNAME_EXT = StyleConstants.ERROR_EXT;
+
+ /**
+ * A string that, if found in a non-JSON response to a UIDL request, will
+ * cause the browser to refresh the page. If followed by a colon, optional
+ * whitespace, and a URI, causes the browser to synchronously load the URI.
+ *
+ * <p>
+ * This allows, for instance, a servlet filter to redirect the application
+ * to a custom login page when the session expires. For example:
+ * </p>
+ *
+ * <pre>
+ * if (sessionExpired) {
+ * response.setHeader("Content-Type", "text/html");
+ * response.getWriter().write(
+ * myLoginPageHtml + "<!-- Vaadin-Refresh: "
+ * + request.getContextPath() + " -->");
+ * }
+ * </pre>
+ */
+ public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh";
+
+ private final HashMap<String, String> resourcesMap = new HashMap<String, String>();
+
+ private WidgetSet widgetSet;
+
+ private VContextMenu contextMenu = null;
+
+ private final UIConnector uIConnector;
+
+ protected boolean cssLoaded = false;
+
+ /** Parameters for this application connection loaded from the web-page */
+ private ApplicationConfiguration configuration;
+
+ private final LayoutManager layoutManager;
+
+ private final RpcManager rpcManager;
+
+ /** Event bus for communication events */
+ private EventBus eventBus = GWT.create(SimpleEventBus.class);
+
+ public enum ApplicationState {
+ INITIALIZING, RUNNING, TERMINATED;
+ }
+
+ private ApplicationState applicationState = ApplicationState.INITIALIZING;
+
+ /**
+ * The communication handler methods are called at certain points during
+ * communication with the server. This allows for making add-ons that keep
+ * track of different aspects of the communication.
+ */
+ public interface CommunicationHandler extends EventHandler {
+ void onRequestStarting(RequestStartingEvent e);
+
+ void onResponseHandlingStarted(ResponseHandlingStartedEvent e);
+
+ void onResponseHandlingEnded(ResponseHandlingEndedEvent e);
+ }
+
+ public static class RequestStartingEvent extends ApplicationConnectionEvent {
+
+ public static Type<CommunicationHandler> TYPE = new Type<CommunicationHandler>();
+
+ public RequestStartingEvent(ApplicationConnection connection) {
+ super(connection);
+ }
+
+ @Override
+ public Type<CommunicationHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(CommunicationHandler handler) {
+ handler.onRequestStarting(this);
+ }
+ }
+
+ public static class ResponseHandlingEndedEvent extends
+ ApplicationConnectionEvent {
+
+ public static Type<CommunicationHandler> TYPE = new Type<CommunicationHandler>();
+
+ public ResponseHandlingEndedEvent(ApplicationConnection connection) {
+ super(connection);
+ }
+
+ @Override
+ public Type<CommunicationHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(CommunicationHandler handler) {
+ handler.onResponseHandlingEnded(this);
+ }
+ }
+
+ public static abstract class ApplicationConnectionEvent extends
+ GwtEvent<CommunicationHandler> {
+
+ private ApplicationConnection connection;
+
+ protected ApplicationConnectionEvent(ApplicationConnection connection) {
+ this.connection = connection;
+ }
+
+ public ApplicationConnection getConnection() {
+ return connection;
+ }
+
+ }
+
+ public static class ResponseHandlingStartedEvent extends
+ ApplicationConnectionEvent {
+
+ public ResponseHandlingStartedEvent(ApplicationConnection connection) {
+ super(connection);
+ }
+
+ public static Type<CommunicationHandler> TYPE = new Type<CommunicationHandler>();
+
+ @Override
+ public Type<CommunicationHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(CommunicationHandler handler) {
+ handler.onResponseHandlingStarted(this);
+ }
+ }
+
+ /**
+ * Event triggered when a application is stopped by calling
+ * {@link ApplicationConnection#setApplicationRunning(false)}.
+ *
+ * To listen for the event add a {@link ApplicationStoppedHandler} by
+ * invoking
+ * {@link ApplicationConnection#addHandler(ApplicationConnection.ApplicationStoppedEvent.Type, ApplicationStoppedHandler)}
+ * to the {@link ApplicationConnection}
+ *
+ * @since 7.1.8
+ * @author Vaadin Ltd
+ */
+ public static class ApplicationStoppedEvent extends
+ GwtEvent<ApplicationStoppedHandler> {
+
+ public static Type<ApplicationStoppedHandler> TYPE = new Type<ApplicationStoppedHandler>();
+
+ @Override
+ public Type<ApplicationStoppedHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(ApplicationStoppedHandler listener) {
+ listener.onApplicationStopped(this);
+ }
+ }
+
+ /**
+ * Allows custom handling of communication errors.
+ */
+ public interface CommunicationErrorHandler {
+ /**
+ * Called when a communication error has occurred. Returning
+ * <code>true</code> from this method suppresses error handling.
+ *
+ * @param details
+ * A string describing the error.
+ * @param statusCode
+ * The HTTP status code (e.g. 404, etc).
+ * @return true if the error reporting should be suppressed, false to
+ * perform normal error reporting.
+ */
+ public boolean onError(String details, int statusCode);
+ }
+
+ /**
+ * A listener for listening to application stopped events. The listener can
+ * be added to a {@link ApplicationConnection} by invoking
+ * {@link ApplicationConnection#addHandler(ApplicationStoppedEvent.Type, ApplicationStoppedHandler)}
+ *
+ * @since 7.1.8
+ * @author Vaadin Ltd
+ */
+ public interface ApplicationStoppedHandler extends EventHandler {
+
+ /**
+ * Triggered when the {@link ApplicationConnection} marks a previously
+ * running application as stopped by invoking
+ * {@link ApplicationConnection#setApplicationRunning(false)}
+ *
+ * @param event
+ * the event triggered by the {@link ApplicationConnection}
+ */
+ void onApplicationStopped(ApplicationStoppedEvent event);
+ }
+
+ private CommunicationErrorHandler communicationErrorDelegate = null;
+
+ private VLoadingIndicator loadingIndicator;
+
+ private Heartbeat heartbeat = GWT.create(Heartbeat.class);
+
+ private boolean tooltipInitialized = false;
+
+ private final VaadinUriResolver uriResolver = new VaadinUriResolver() {
+ @Override
+ protected String getVaadinDirUrl() {
+ return getConfiguration().getVaadinDirUrl();
+ }
+
+ @Override
+ protected String getServiceUrlParameterName() {
+ return getConfiguration().getServiceUrlParameterName();
+ }
+
+ @Override
+ protected String getServiceUrl() {
+ return getConfiguration().getServiceUrl();
+ }
+
+ @Override
+ protected String getThemeUri() {
+ return ApplicationConnection.this.getThemeUri();
+ }
+
+ @Override
+ protected String encodeQueryStringParameterValue(String queryString) {
+ return URL.encodeQueryString(queryString);
+ }
+ };
+
+ public static class MultiStepDuration extends Duration {
+ private int previousStep = elapsedMillis();
+
+ public void logDuration(String message) {
+ logDuration(message, 0);
+ }
+
+ public void logDuration(String message, int minDuration) {
+ int currentTime = elapsedMillis();
+ int stepDuration = currentTime - previousStep;
+ if (stepDuration >= minDuration) {
+ getLogger().info(message + ": " + stepDuration + " ms");
+ }
+ previousStep = currentTime;
+ }
+ }
+
+ public ApplicationConnection() {
+ // Assuming UI data is eagerly loaded
+ ConnectorBundleLoader.get().loadBundle(
+ ConnectorBundleLoader.EAGER_BUNDLE_NAME, null);
+ uIConnector = GWT.create(UIConnector.class);
+ rpcManager = GWT.create(RpcManager.class);
+ layoutManager = GWT.create(LayoutManager.class);
+ tooltip = GWT.create(VTooltip.class);
+ loadingIndicator = GWT.create(VLoadingIndicator.class);
+ serverRpcQueue = GWT.create(ServerRpcQueue.class);
+ connectionStateHandler = GWT.create(ConnectionStateHandler.class);
+ messageHandler = GWT.create(MessageHandler.class);
+ messageSender = GWT.create(MessageSender.class);
+ }
+
+ public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) {
+ getLogger().info("Starting application " + cnf.getRootPanelId());
+ getLogger().info("Using theme: " + cnf.getThemeName());
+
+ getLogger().info(
+ "Vaadin application servlet version: "
+ + cnf.getServletVersion());
+
+ if (!cnf.getServletVersion().equals(Version.getFullVersion())) {
+ getLogger()
+ .severe("Warning: your widget set seems to be built with a different "
+ + "version than the one used on server. Unexpected "
+ + "behavior may occur.");
+ }
+
+ this.widgetSet = widgetSet;
+ configuration = cnf;
+
+ layoutManager.setConnection(this);
+ loadingIndicator.setConnection(this);
+ serverRpcQueue.setConnection(this);
+ messageHandler.setConnection(this);
+ messageSender.setConnection(this);
+
+ ComponentLocator componentLocator = new ComponentLocator(this);
+
+ String appRootPanelName = cnf.getRootPanelId();
+ // remove the end (window name) of autogenerated rootpanel id
+ appRootPanelName = appRootPanelName.replaceFirst("-\\d+$", "");
+
+ initializeTestbenchHooks(componentLocator, appRootPanelName);
+
+ initializeClientHooks();
+
+ uIConnector.init(cnf.getRootPanelId(), this);
+
+ // Connection state handler preloads the reconnect dialog, which uses
+ // overlay container. This in turn depends on VUI being attached
+ // (done in uiConnector.init)
+ connectionStateHandler.setConnection(this);
+
+ tooltip.setOwner(uIConnector.getWidget());
+
+ getLoadingIndicator().show();
+
+ heartbeat.init(this);
+
+ // Ensure the overlay container is added to the dom and set as a live
+ // area for assistive devices
+ Element overlayContainer = VOverlay.getOverlayContainer(this);
+ Roles.getAlertRole().setAriaLiveProperty(overlayContainer,
+ LiveValue.ASSERTIVE);
+ VOverlay.setOverlayContainerLabel(this,
+ getUIConnector().getState().overlayContainerLabel);
+ Roles.getAlertRole().setAriaRelevantProperty(overlayContainer,
+ RelevantValue.ADDITIONS);
+ }
+
+ /**
+ * Starts this application. Don't call this method directly - it's called by
+ * {@link ApplicationConfiguration#startNextApplication()}, which should be
+ * called once this application has started (first response received) or
+ * failed to start. This ensures that the applications are started in order,
+ * to avoid session-id problems.
+ *
+ */
+ public void start() {
+ String jsonText = configuration.getUIDL();
+ if (jsonText == null) {
+ // initial UIDL not in DOM, request from server
+ getMessageSender().resynchronize();
+ } else {
+ // initial UIDL provided in DOM, continue as if returned by request
+
+ // Hack to avoid logging an error in endRequest()
+ getMessageSender().startRequest();
+ getMessageHandler().handleMessage(
+ MessageHandler.parseJson(jsonText));
+ }
+
+ // Tooltip can't be created earlier because the
+ // necessary fields are not setup to add it in the
+ // correct place in the DOM
+ if (!tooltipInitialized) {
+ tooltipInitialized = true;
+ ApplicationConfiguration.runWhenDependenciesLoaded(new Command() {
+ @Override
+ public void execute() {
+ getVTooltip().initializeAssistiveTooltips();
+ }
+ });
+ }
+ }
+
+ /**
+ * Checks if there is some work to be done on the client side
+ *
+ * @return true if the client has some work to be done, false otherwise
+ */
+ private boolean isActive() {
+ return !getMessageHandler().isInitialUidlHandled() || isWorkPending()
+ || getMessageSender().hasActiveRequest()
+ || isExecutingDeferredCommands();
+ }
+
+ private native void initializeTestbenchHooks(
+ ComponentLocator componentLocator, String TTAppId)
+ /*-{
+ var ap = this;
+ var client = {};
+ client.isActive = $entry(function() {
+ return ap.@com.vaadin.client.ApplicationConnection::isActive()();
+ });
+ var vi = ap.@com.vaadin.client.ApplicationConnection::getVersionInfo()();
+ if (vi) {
+ client.getVersionInfo = function() {
+ return vi;
+ }
+ }
+
+ client.getProfilingData = $entry(function() {
- pd = pd.concat(smh.@com.vaadin.client.communication.MessageHandler::serverTimingInfo);
++ var smh = ap.@com.vaadin.client.ApplicationConnection::getMessageHandler()();
+ var pd = [
+ smh.@com.vaadin.client.communication.MessageHandler::lastProcessingTime,
+ smh.@com.vaadin.client.communication.MessageHandler::totalProcessingTime
+ ];
++ if (null != smh.@com.vaadin.client.communication.MessageHandler::serverTimingInfo) {
++ pd = pd.concat(smh.@com.vaadin.client.communication.MessageHandler::serverTimingInfo);
++ } else {
++ pd = pd.concat(-1, -1);
++ }
+ pd[pd.length] = smh.@com.vaadin.client.communication.MessageHandler::bootstrapTime;
+ return pd;
+ });
+
+ client.getElementByPath = $entry(function(id) {
+ return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
+ });
+ client.getElementByPathStartingAt = $entry(function(id, element) {
+ return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementByPathStartingAt(Ljava/lang/String;Lcom/google/gwt/dom/client/Element;)(id, element);
+ });
+ client.getElementsByPath = $entry(function(id) {
+ return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementsByPath(Ljava/lang/String;)(id);
+ });
+ client.getElementsByPathStartingAt = $entry(function(id, element) {
+ return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementsByPathStartingAt(Ljava/lang/String;Lcom/google/gwt/dom/client/Element;)(id, element);
+ });
+ client.getPathForElement = $entry(function(element) {
+ return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getPathForElement(Lcom/google/gwt/dom/client/Element;)(element);
+ });
+ client.initializing = false;
+
+ $wnd.vaadin.clients[TTAppId] = client;
+ }-*/;
+
+ /**
+ * Helper for tt initialization
+ */
+ private JavaScriptObject getVersionInfo() {
+ return configuration.getVersionInfoJSObject();
+ }
+
+ /**
+ * Publishes a JavaScript API for mash-up applications.
+ * <ul>
+ * <li><code>vaadin.forceSync()</code> sends pending variable changes, in
+ * effect synchronizing the server and client state. This is done for all
+ * applications on host page.</li>
+ * <li><code>vaadin.postRequestHooks</code> is a map of functions which gets
+ * called after each XHR made by vaadin application. Note, that it is
+ * attaching js functions responsibility to create the variable like this:
+ *
+ * <code><pre>
+ * if(!vaadin.postRequestHooks) {vaadin.postRequestHooks = new Object();}
+ * postRequestHooks.myHook = function(appId) {
+ * if(appId == "MyAppOfInterest") {
+ * // do the staff you need on xhr activity
+ * }
+ * }
+ * </pre></code> First parameter passed to these functions is the identifier
+ * of Vaadin application that made the request.
+ * </ul>
+ *
+ * TODO make this multi-app aware
+ */
+ private native void initializeClientHooks()
+ /*-{
+ var app = this;
+ var oldSync;
+ if ($wnd.vaadin.forceSync) {
+ oldSync = $wnd.vaadin.forceSync;
+ }
+ $wnd.vaadin.forceSync = $entry(function() {
+ if (oldSync) {
+ oldSync();
+ }
+ app.@com.vaadin.client.ApplicationConnection::sendPendingVariableChanges()();
+ });
+ var oldForceLayout;
+ if ($wnd.vaadin.forceLayout) {
+ oldForceLayout = $wnd.vaadin.forceLayout;
+ }
+ $wnd.vaadin.forceLayout = $entry(function() {
+ if (oldForceLayout) {
+ oldForceLayout();
+ }
+ app.@com.vaadin.client.ApplicationConnection::forceLayout()();
+ });
+ }-*/;
+
+ /**
+ * Requests an analyze of layouts, to find inconsistencies. Exclusively used
+ * for debugging during development.
+ *
+ * @deprecated as of 7.1. Replaced by {@link UIConnector#analyzeLayouts()}
+ */
+ @Deprecated
+ public void analyzeLayouts() {
+ getUIConnector().analyzeLayouts();
+ }
+
+ /**
+ * Sends a request to the server to print details to console that will help
+ * the developer to locate the corresponding server-side connector in the
+ * source code.
+ *
+ * @param serverConnector
+ * @deprecated as of 7.1. Replaced by
+ * {@link UIConnector#showServerDebugInfo(ServerConnector)}
+ */
+ @Deprecated
+ void highlightConnector(ServerConnector serverConnector) {
+ getUIConnector().showServerDebugInfo(serverConnector);
+ }
+
+ int cssWaits = 0;
+
+ protected ServerRpcQueue serverRpcQueue;
+ protected ConnectionStateHandler connectionStateHandler;
+ protected MessageHandler messageHandler;
+ protected MessageSender messageSender;
+
+ static final int MAX_CSS_WAITS = 100;
+
+ public void executeWhenCSSLoaded(final Command c) {
+ if (!isCSSLoaded() && cssWaits < MAX_CSS_WAITS) {
+ (new Timer() {
+ @Override
+ public void run() {
+ executeWhenCSSLoaded(c);
+ }
+ }).schedule(50);
+
+ // Show this message just once
+ if (cssWaits++ == 0) {
+ getLogger().warning(
+ "Assuming CSS loading is not complete, "
+ + "postponing render phase. "
+ + "(.v-loading-indicator height == 0)");
+ }
+ } else {
+ cssLoaded = true;
+ if (cssWaits >= MAX_CSS_WAITS) {
+ getLogger().severe("CSS files may have not loaded properly.");
+ }
+
+ c.execute();
+ }
+ }
+
+ /**
+ * Checks whether or not the CSS is loaded. By default checks the size of
+ * the loading indicator element.
+ *
+ * @return
+ */
+ protected boolean isCSSLoaded() {
+ return cssLoaded
+ || getLoadingIndicator().getElement().getOffsetHeight() != 0;
+ }
+
+ /**
+ * Shows the communication error notification.
+ *
+ * @param details
+ * Optional details.
+ * @param statusCode
+ * The status code returned for the request
+ *
+ */
+ public void showCommunicationError(String details, int statusCode) {
+ getLogger().severe("Communication error: " + details);
+ showError(details, configuration.getCommunicationError());
+ }
+
+ /**
+ * Shows the authentication error notification.
+ *
+ * @param details
+ * Optional details.
+ */
+ public void showAuthenticationError(String details) {
+ getLogger().severe("Authentication error: " + details);
+ showError(details, configuration.getAuthorizationError());
+ }
+
+ /**
+ * Shows the session expiration notification.
+ *
+ * @param details
+ * Optional details.
+ */
+ public void showSessionExpiredError(String details) {
+ getLogger().severe("Session expired: " + details);
+ showError(details, configuration.getSessionExpiredError());
+ }
+
+ /**
+ * Shows an error notification.
+ *
+ * @param details
+ * Optional details.
+ * @param message
+ * An ErrorMessage describing the error.
+ */
+ protected void showError(String details, ErrorMessage message) {
+ VNotification.showError(this, message.getCaption(),
+ message.getMessage(), details, message.getUrl());
+ }
+
+ /**
+ * Checks if the client has running or scheduled commands
+ */
+ private boolean isWorkPending() {
+ ConnectorMap connectorMap = getConnectorMap();
+ JsArrayObject<ServerConnector> connectors = connectorMap
+ .getConnectorsAsJsArray();
+ int size = connectors.size();
+ for (int i = 0; i < size; i++) {
+ ServerConnector conn = connectors.get(i);
+ if (isWorkPending(conn)) {
+ return true;
+ }
+
+ if (conn instanceof ComponentConnector) {
+ ComponentConnector compConn = (ComponentConnector) conn;
+ if (isWorkPending(compConn.getWidget())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean isWorkPending(Object object) {
+ return object instanceof DeferredWorker
+ && ((DeferredWorker) object).isWorkPending();
+ }
+
+ /**
+ * Checks if deferred commands are (potentially) still being executed as a
+ * result of an update from the server. Returns true if a deferred command
+ * might still be executing, false otherwise. This will not work correctly
+ * if a deferred command is added in another deferred command.
+ * <p>
+ * Used by the native "client.isActive" function.
+ * </p>
+ *
+ * @return true if deferred commands are (potentially) being executed, false
+ * otherwise
+ */
+ private boolean isExecutingDeferredCommands() {
+ Scheduler s = Scheduler.get();
+ if (s instanceof VSchedulerImpl) {
+ return ((VSchedulerImpl) s).hasWorkQueued();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the loading indicator used by this ApplicationConnection
+ *
+ * @return The loading indicator for this ApplicationConnection
+ */
+ public VLoadingIndicator getLoadingIndicator() {
+ return loadingIndicator;
+ }
+
+ /**
+ * Determines whether or not the loading indicator is showing.
+ *
+ * @return true if the loading indicator is visible
+ * @deprecated As of 7.1. Use {@link #getLoadingIndicator()} and
+ * {@link VLoadingIndicator#isVisible()}.isVisible() instead.
+ */
+ @Deprecated
+ public boolean isLoadingIndicatorVisible() {
+ return getLoadingIndicator().isVisible();
+ }
+
+ public void loadStyleDependencies(JsArrayString dependencies) {
+ // Assuming no reason to interpret in a defined order
+ ResourceLoadListener resourceLoadListener = new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ ApplicationConfiguration.endDependencyLoading();
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ getLogger()
+ .severe(event.getResourceUrl()
+ + " could not be loaded, or the load detection failed because the stylesheet is empty.");
+ // The show must go on
+ onLoad(event);
+ }
+ };
+ ResourceLoader loader = ResourceLoader.get();
+ for (int i = 0; i < dependencies.length(); i++) {
+ String url = translateVaadinUri(dependencies.get(i));
+ ApplicationConfiguration.startDependencyLoading();
+ loader.loadStylesheet(url, resourceLoadListener);
+ }
+ }
+
+ public void loadScriptDependencies(final JsArrayString dependencies) {
+ if (dependencies.length() == 0) {
+ return;
+ }
+
+ // Listener that loads the next when one is completed
+ ResourceLoadListener resourceLoadListener = new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ if (dependencies.length() != 0) {
+ String url = translateVaadinUri(dependencies.shift());
+ ApplicationConfiguration.startDependencyLoading();
+ // Load next in chain (hopefully already preloaded)
+ event.getResourceLoader().loadScript(url, this);
+ }
+ // Call start for next before calling end for current
+ ApplicationConfiguration.endDependencyLoading();
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ getLogger().severe(
+ event.getResourceUrl() + " could not be loaded.");
+ // The show must go on
+ onLoad(event);
+ }
+ };
+
+ ResourceLoader loader = ResourceLoader.get();
+
+ // Start chain by loading first
+ String url = translateVaadinUri(dependencies.shift());
+ ApplicationConfiguration.startDependencyLoading();
+ loader.loadScript(url, resourceLoadListener);
+
+ if (ResourceLoader.supportsInOrderScriptExecution()) {
+ for (int i = 0; i < dependencies.length(); i++) {
+ String preloadUrl = translateVaadinUri(dependencies.get(i));
+ loader.loadScript(preloadUrl, null);
+ }
+ } else {
+ // Preload all remaining
+ for (int i = 0; i < dependencies.length(); i++) {
+ String preloadUrl = translateVaadinUri(dependencies.get(i));
+ loader.preloadResource(preloadUrl, null);
+ }
+ }
+ }
+
+ private void addVariableToQueue(String connectorId, String variableName,
+ Object value, boolean immediate) {
+ boolean lastOnly = !immediate;
+ // note that type is now deduced from value
+ serverRpcQueue.add(new LegacyChangeVariablesInvocation(connectorId,
+ variableName, value), lastOnly);
+ if (immediate) {
+ serverRpcQueue.flush();
+ }
+ }
+
+ /**
+ * @deprecated as of 7.6, use {@link ServerRpcQueue#flush()}
+ */
+ @Deprecated
+ public void sendPendingVariableChanges() {
+ serverRpcQueue.flush();
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+ public void updateVariable(String paintableId, String variableName,
+ ServerConnector newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ String newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ int newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ long newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ float newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ double newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ boolean newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param map
+ * the new values to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+ public void updateVariable(String paintableId, String variableName,
+ Map<String, Object> map, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, map, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * <p>
+ * A null array is sent as an empty array.
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param values
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+ public void updateVariable(String paintableId, String variableName,
+ String[] values, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, values, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * <p>
+ * A null array is sent as an empty array.
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param values
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+ public void updateVariable(String paintableId, String variableName,
+ Object[] values, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, values, immediate);
+ }
+
+ /**
+ * Does absolutely nothing. Replaced by {@link LayoutManager}.
+ *
+ * @param container
+ * @deprecated As of 7.0, serves no purpose
+ */
+ @Deprecated
+ public void runDescendentsLayout(HasWidgets container) {
+ }
+
+ /**
+ * This will cause re-layouting of all components. Mainly used for
+ * development. Published to JavaScript.
+ */
+ public void forceLayout() {
+ Duration duration = new Duration();
+
+ layoutManager.forceLayout();
+
+ getLogger().info("forceLayout in " + duration.elapsedMillis() + " ms");
+ }
+
+ /**
+ * Returns false
+ *
+ * @param paintable
+ * @return false, always
+ * @deprecated As of 7.0, serves no purpose
+ */
+ @Deprecated
+ private boolean handleComponentRelativeSize(ComponentConnector paintable) {
+ return false;
+ }
+
+ /**
+ * Returns false
+ *
+ * @param paintable
+ * @return false, always
+ * @deprecated As of 7.0, serves no purpose
+ */
+ @Deprecated
+ public boolean handleComponentRelativeSize(Widget widget) {
+ return handleComponentRelativeSize(connectorMap.getConnector(widget));
+
+ }
+
+ @Deprecated
+ public ComponentConnector getPaintable(UIDL uidl) {
+ // Non-component connectors shouldn't be painted from legacy connectors
+ return (ComponentConnector) getConnector(uidl.getId(),
+ Integer.parseInt(uidl.getTag()));
+ }
+
+ /**
+ * Get either an existing ComponentConnector or create a new
+ * ComponentConnector with the given type and id.
+ *
+ * If a ComponentConnector with the given id already exists, returns it.
+ * Otherwise creates and registers a new ComponentConnector of the given
+ * type.
+ *
+ * @param connectorId
+ * Id of the paintable
+ * @param connectorType
+ * Type of the connector, as passed from the server side
+ *
+ * @return Either an existing ComponentConnector or a new ComponentConnector
+ * of the given type
+ */
+ public ServerConnector getConnector(String connectorId, int connectorType) {
+ if (!connectorMap.hasConnector(connectorId)) {
+ return createAndRegisterConnector(connectorId, connectorType);
+ }
+ return connectorMap.getConnector(connectorId);
+ }
+
+ /**
+ * Creates a new ServerConnector with the given type and id.
+ *
+ * Creates and registers a new ServerConnector of the given type. Should
+ * never be called with the connector id of an existing connector.
+ *
+ * @param connectorId
+ * Id of the new connector
+ * @param connectorType
+ * Type of the connector, as passed from the server side
+ *
+ * @return A new ServerConnector of the given type
+ */
+ private ServerConnector createAndRegisterConnector(String connectorId,
+ int connectorType) {
+ Profiler.enter("ApplicationConnection.createAndRegisterConnector");
+
+ // Create and register a new connector with the given type
+ ServerConnector p = widgetSet.createConnector(connectorType,
+ configuration);
+ connectorMap.registerConnector(connectorId, p);
+ p.doInit(connectorId, this);
+
+ Profiler.leave("ApplicationConnection.createAndRegisterConnector");
+ return p;
+ }
+
+ /**
+ * Gets a resource that has been pre-loaded via UIDL, such as custom
+ * layouts.
+ *
+ * @param name
+ * identifier of the resource to get
+ * @return the resource
+ */
+ public String getResource(String name) {
+ return resourcesMap.get(name);
+ }
+
+ /**
+ * Sets a resource that has been pre-loaded via UIDL, such as custom
+ * layouts.
+ *
+ * @since 7.6
+ * @param name
+ * identifier of the resource to Set
+ * @param resource
+ * the resource
+ */
+ public void setResource(String name, String resource) {
+ resourcesMap.put(name, resource);
+ }
+
+ /**
+ * Singleton method to get instance of app's context menu.
+ *
+ * @return VContextMenu object
+ */
+ public VContextMenu getContextMenu() {
+ if (contextMenu == null) {
+ contextMenu = new VContextMenu();
+ contextMenu.setOwner(uIConnector.getWidget());
+ DOM.setElementProperty(contextMenu.getElement(), "id",
+ "PID_VAADIN_CM");
+ }
+ return contextMenu;
+ }
+
+ /**
+ * Gets an {@link Icon} instance corresponding to a URI.
+ *
+ * @since 7.2
+ * @param uri
+ * @return Icon object
+ */
+ public Icon getIcon(String uri) {
+ Icon icon;
+ if (uri == null) {
+ return null;
+ } else if (FontIcon.isFontIconUri(uri)) {
+ icon = GWT.create(FontIcon.class);
+ } else {
+ icon = GWT.create(ImageIcon.class);
+ }
+ icon.setUri(translateVaadinUri(uri));
+ return icon;
+ }
+
+ /**
+ * Translates custom protocols in UIDL URI's to be recognizable by browser.
+ * All uri's from UIDL should be routed via this method before giving them
+ * to browser due URI's in UIDL may contain custom protocols like theme://.
+ *
+ * @param uidlUri
+ * Vaadin URI from uidl
+ * @return translated URI ready for browser
+ */
+ public String translateVaadinUri(String uidlUri) {
+ return uriResolver.resolveVaadinUri(uidlUri);
+ }
+
+ /**
+ * Gets the URI for the current theme. Can be used to reference theme
+ * resources.
+ *
+ * @return URI to the current theme
+ */
+ public String getThemeUri() {
+ return configuration.getVaadinDirUrl() + "themes/"
+ + getUIConnector().getActiveTheme();
+ }
+
+ /* Extended title handling */
+
+ private final VTooltip tooltip;
+
+ private ConnectorMap connectorMap = GWT.create(ConnectorMap.class);
+
+ /**
+ * Use to notify that the given component's caption has changed; layouts may
+ * have to be recalculated.
+ *
+ * @param component
+ * the Paintable whose caption has changed
+ * @deprecated As of 7.0.2, has not had any effect for a long time
+ */
+ @Deprecated
+ public void captionSizeUpdated(Widget widget) {
+ // This doesn't do anything, it's just kept here for compatibility
+ }
+
+ /**
+ * Gets the main view
+ *
+ * @return the main view
+ */
+ public UIConnector getUIConnector() {
+ return uIConnector;
+ }
+
+ /**
+ * Gets the {@link ApplicationConfiguration} for the current application.
+ *
+ * @see ApplicationConfiguration
+ * @return the configuration for this application
+ */
+ public ApplicationConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * Checks if there is a registered server side listener for the event. The
+ * list of events which has server side listeners is updated automatically
+ * before the component is updated so the value is correct if called from
+ * updatedFromUIDL.
+ *
+ * @param connector
+ * The connector to register event listeners for
+ * @param eventIdentifier
+ * The identifier for the event
+ * @return true if at least one listener has been registered on server side
+ * for the event identified by eventIdentifier.
+ * @deprecated As of 7.0. Use
+ * {@link AbstractConnector#hasEventListener(String)} instead
+ */
+ @Deprecated
+ public boolean hasEventListeners(ComponentConnector connector,
+ String eventIdentifier) {
+ return connector.hasEventListener(eventIdentifier);
+ }
+
+ /**
+ * Adds the get parameters to the uri and returns the new uri that contains
+ * the parameters.
+ *
+ * @param uri
+ * The uri to which the parameters should be added.
+ * @param extraParams
+ * One or more parameters in the format "a=b" or "c=d&e=f". An
+ * empty string is allowed but will not modify the url.
+ * @return The modified URI with the get parameters in extraParams added.
+ * @deprecated Use {@link SharedUtil#addGetParameters(String,String)}
+ * instead
+ */
+ @Deprecated
+ public static String addGetParameters(String uri, String extraParams) {
+ return SharedUtil.addGetParameters(uri, extraParams);
+ }
+
+ ConnectorMap getConnectorMap() {
+ return connectorMap;
+ }
+
+ /**
+ * @deprecated As of 7.0. No longer serves any purpose.
+ */
+ @Deprecated
+ public void unregisterPaintable(ServerConnector p) {
+ getLogger().info(
+ "unregisterPaintable (unnecessarily) called for "
+ + Util.getConnectorString(p));
+ }
+
+ /**
+ * Get VTooltip instance related to application connection
+ *
+ * @return VTooltip instance
+ */
+ public VTooltip getVTooltip() {
+ return tooltip;
+ }
+
+ /**
+ * Method provided for backwards compatibility. Duties previously done by
+ * this method is now handled by the state change event handler in
+ * AbstractComponentConnector. The only function this method has is to
+ * return true if the UIDL is a "cached" update.
+ *
+ * @param component
+ * @param uidl
+ * @param manageCaption
+ * @deprecated As of 7.0, no longer serves any purpose
+ * @return
+ */
+ @Deprecated
+ public boolean updateComponent(Widget component, UIDL uidl,
+ boolean manageCaption) {
+ ComponentConnector connector = getConnectorMap()
+ .getConnector(component);
+ if (!AbstractComponentConnector.isRealUpdate(uidl)) {
+ return true;
+ }
+
+ if (!manageCaption) {
+ getLogger()
+ .warning(
+ Util.getConnectorString(connector)
+ + " called updateComponent with manageCaption=false. The parameter was ignored - override delegateCaption() to return false instead. It is however not recommended to use caption this way at all.");
+ }
+ return false;
+ }
+
+ /**
+ * @deprecated As of 7.0. Use
+ * {@link AbstractComponentConnector#hasEventListener(String)}
+ * instead
+ */
+ @Deprecated
+ public boolean hasEventListeners(Widget widget, String eventIdentifier) {
+ ComponentConnector connector = getConnectorMap().getConnector(widget);
+ if (connector == null) {
+ /*
+ * No connector will exist in cases where Vaadin widgets have been
+ * re-used without implementing server<->client communication.
+ */
+ return false;
+ }
+
+ return hasEventListeners(getConnectorMap().getConnector(widget),
+ eventIdentifier);
+ }
+
+ LayoutManager getLayoutManager() {
+ return layoutManager;
+ }
+
+ /**
+ * Schedules a heartbeat request to occur after the configured heartbeat
+ * interval elapses if the interval is a positive number. Otherwise, does
+ * nothing.
+ *
+ * @deprecated as of 7.2, use {@link Heartbeat#schedule()} instead
+ */
+ @Deprecated
+ protected void scheduleHeartbeat() {
+ heartbeat.schedule();
+ }
+
+ /**
+ * Sends a heartbeat request to the server.
+ * <p>
+ * Heartbeat requests are used to inform the server that the client-side is
+ * still alive. If the client page is closed or the connection lost, the
+ * server will eventually close the inactive UI.
+ *
+ * @deprecated as of 7.2, use {@link Heartbeat#send()} instead
+ */
+ @Deprecated
+ protected void sendHeartbeat() {
+ heartbeat.send();
+ }
+
+ public void handleCommunicationError(String details, int statusCode) {
+ boolean handled = false;
+ if (communicationErrorDelegate != null) {
+ handled = communicationErrorDelegate.onError(details, statusCode);
+
+ }
+
+ if (!handled) {
+ showCommunicationError(details, statusCode);
+ }
+
+ }
+
+ /**
+ * Sets the delegate that is called whenever a communication error occurrs.
+ *
+ * @param delegate
+ * the delegate.
+ */
+ public void setCommunicationErrorDelegate(CommunicationErrorHandler delegate) {
+ communicationErrorDelegate = delegate;
+ }
+
+ public void setApplicationRunning(boolean applicationRunning) {
+ if (getApplicationState() == ApplicationState.TERMINATED) {
+ if (applicationRunning) {
+ getLogger()
+ .severe("Tried to restart a terminated application. This is not supported");
+ } else {
+ getLogger()
+ .warning(
+ "Tried to stop a terminated application. This should not be done");
+ }
+ return;
+ } else if (getApplicationState() == ApplicationState.INITIALIZING) {
+ if (applicationRunning) {
+ applicationState = ApplicationState.RUNNING;
+ } else {
+ getLogger()
+ .warning(
+ "Tried to stop the application before it has started. This should not be done");
+ }
+ } else if (getApplicationState() == ApplicationState.RUNNING) {
+ if (!applicationRunning) {
+ applicationState = ApplicationState.TERMINATED;
+ eventBus.fireEvent(new ApplicationStoppedEvent());
+ } else {
+ getLogger()
+ .warning(
+ "Tried to start an already running application. This should not be done");
+ }
+ }
+ }
+
+ /**
+ * Checks if the application is in the {@link ApplicationState#RUNNING}
+ * state.
+ *
+ * @since 7.6
+ * @return true if the application is in the running state, false otherwise
+ */
+ public boolean isApplicationRunning() {
+ return applicationState == ApplicationState.RUNNING;
+ }
+
+ public <H extends EventHandler> HandlerRegistration addHandler(
+ GwtEvent.Type<H> type, H handler) {
+ return eventBus.addHandler(type, handler);
+ }
+
+ @Override
+ public void fireEvent(GwtEvent<?> event) {
+ eventBus.fireEvent(event);
+ }
+
+ /**
+ * Calls {@link ComponentConnector#flush()} on the active connector. Does
+ * nothing if there is no active (focused) connector.
+ */
+ public void flushActiveConnector() {
+ ComponentConnector activeConnector = getActiveConnector();
+ if (activeConnector == null) {
+ return;
+ }
+ activeConnector.flush();
+ }
+
+ /**
+ * Gets the active connector for focused element in browser.
+ *
+ * @return Connector for focused element or null.
+ */
+ private ComponentConnector getActiveConnector() {
+ Element focusedElement = WidgetUtil.getFocusedElement();
+ if (focusedElement == null) {
+ return null;
+ }
+ return Util.getConnectorForElement(this, getUIConnector().getWidget(),
+ focusedElement);
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(ApplicationConnection.class.getName());
+ }
+
+ /**
+ * Returns the hearbeat instance.
+ */
+ public Heartbeat getHeartbeat() {
+ return heartbeat;
+ }
+
+ /**
+ * Returns the state of this application. An application state goes from
+ * "initializing" to "running" to "stopped". There is no way for an
+ * application to go back to a previous state, i.e. a stopped application
+ * can never be re-started
+ *
+ * @since 7.6
+ * @return the current state of this application
+ */
+ public ApplicationState getApplicationState() {
+ return applicationState;
+ }
+
+ /**
+ * Gets the server RPC queue for this application
+ *
+ * @since 7.6
+ * @return the server RPC queue
+ */
+ public ServerRpcQueue getServerRpcQueue() {
+ return serverRpcQueue;
+ }
+
+ /**
+ * Gets the communication error handler for this application
+ *
+ * @since 7.6
+ * @return the server RPC queue
+ */
+ public ConnectionStateHandler getConnectionStateHandler() {
+ return connectionStateHandler;
+ }
+
+ /**
+ * Gets the (server to client) message handler for this application
+ *
+ * @since 7.6
+ * @return the message handler
+ */
+ public MessageHandler getMessageHandler() {
+ return messageHandler;
+ }
+
+ /**
+ * Gets the server rpc manager for this application
+ *
+ * @since 7.6
+ * @return the server rpc manager
+ */
+ public RpcManager getRpcManager() {
+ return rpcManager;
+ }
+
+ /**
+ * Gets the (client to server) message sender for this application
+ *
+ * @since 7.6
+ * @return the message sender
+ */
+ public MessageSender getMessageSender() {
+ return messageSender;
+ }
+
+ /**
+ * @since 7.6
+ * @return the widget set
+ */
+ public WidgetSet getWidgetSet() {
+ return widgetSet;
+ }
+
+ public int getLastSeenServerSyncId() {
+ return getMessageHandler().getLastSeenServerSyncId();
+ }
+
+}
--- /dev/null
- AbstractRendererConnector<JsonValue> implements
+/*
+ * Copyright 2000-2014 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.client.connectors;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.NativeEvent;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.JavaScriptConnectorHelper;
+import com.vaadin.client.Util;
+import com.vaadin.client.communication.HasJavaScriptConnectorHelper;
+import com.vaadin.client.renderers.ComplexRenderer;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.ui.renderers.AbstractJavaScriptRenderer;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * Connector for server-side renderer implemented using JavaScript.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
++// This is really typed to <JsonValue>, but because of the way native strings
++// are not always instanceof JsonValue, we need to accept Object
+@Connect(AbstractJavaScriptRenderer.class)
+public class JavaScriptRendererConnector extends
- protected Renderer<JsonValue> createRenderer() {
++ AbstractRendererConnector<Object> implements
+ HasJavaScriptConnectorHelper {
+ private final JavaScriptConnectorHelper helper = new JavaScriptConnectorHelper(
+ this);
+
+ private final JavaScriptObject cellReferenceWrapper = createCellReferenceWrapper(BrowserInfo
+ .get().isIE8());
+
+ @Override
+ protected void init() {
+ super.init();
+ helper.init();
+
+ addGetRowKey(helper.getConnectorWrapper());
+ }
+
+ private static native JavaScriptObject createCellReferenceWrapper(
+ boolean isIE8)
+ /*-{
+ var reference = {};
+ if (isIE8) {
+ // IE8 only supports defineProperty for DOM objects
+ reference = $doc.createElement('div');
+ }
+
+ var setProperty = function(name, getter, setter) {
+ var descriptor = {
+ get: getter
+ }
+ if (setter) {
+ descriptor.set = setter;
+ }
+ Object.defineProperty(reference, name, descriptor);
+ };
+
+ setProperty("element", function() {
+ return reference.target.@CellReference::getElement()();
+ }, null);
+
+ setProperty("rowIndex", function() {
+ return reference.target.@CellReference::getRowIndex()();
+ }, null);
+
+ setProperty("columnIndex", function() {
+ return reference.target.@CellReference::getColumnIndex()();
+ }, null);
+
+ setProperty("colSpan", function() {
+ return reference.target.@RendererCellReference::getColSpan()();
+ }, function(colSpan) {
+ reference.target.@RendererCellReference::setColSpan(*)(colSpan);
+ });
+
+ return reference;
+ }-*/;
+
+ @Override
+ public JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+
+ private native void addGetRowKey(JavaScriptObject wrapper)
+ /*-{
+ var self = this;
+ wrapper.getRowKey = $entry(function(rowIndex) {
+ return @JavaScriptRendererConnector::findRowKey(*)(self, rowIndex);
+ });
+ }-*/;
+
+ private static String findRowKey(JavaScriptRendererConnector connector,
+ int rowIndex) {
+ GridConnector gc = (GridConnector) connector.getParent();
+ JsonObject row = gc.getWidget().getDataSource().getRow(rowIndex);
+ return connector.getRowKey(row);
+ }
+
+ private boolean hasFunction(String name) {
+ return hasFunction(helper.getConnectorWrapper(), name);
+ }
+
+ private static native boolean hasFunction(JavaScriptObject wrapper,
+ String name)
+ /*-{
+ return typeof wrapper[name] === 'function';
+ }-*/;
+
+ @Override
- return new ComplexRenderer<JsonValue>() {
++ protected Renderer<Object> createRenderer() {
+ helper.ensureJavascriptInited();
+
+ if (!hasFunction("render")) {
+ throw new RuntimeException("JavaScriptRenderer "
+ + helper.getInitFunctionName()
+ + " must have a function named 'render'");
+ }
+
+ final boolean hasInit = hasFunction("init");
+ final boolean hasDestroy = hasFunction("destroy");
+ final boolean hasOnActivate = hasFunction("onActivate");
+ final boolean hasGetConsumedEvents = hasFunction("getConsumedEvents");
+ final boolean hasOnBrowserEvent = hasFunction("onBrowserEvent");
+
- public void render(RendererCellReference cell, JsonValue data) {
- render(helper.getConnectorWrapper(), getJsCell(cell),
- Util.json2jso(data));
++ return new ComplexRenderer<Object>() {
+ @Override
- JavaScriptObject cell, JavaScriptObject data)
++ public void render(RendererCellReference cell, Object data) {
++ if (data instanceof JsonValue) {
++ data = Util.json2jso((JsonValue) data);
++ }
++ render(helper.getConnectorWrapper(), getJsCell(cell), data);
+ }
+
+ private JavaScriptObject getJsCell(CellReference<?> cell) {
+ updateCellReference(cellReferenceWrapper, cell);
+ return cellReferenceWrapper;
+ }
+
+ public native void render(JavaScriptObject wrapper,
- public JsonValue decode(JsonValue value) {
++ JavaScriptObject cell, Object data)
+ /*-{
+ wrapper.render(cell, data);
+ }-*/;
+
+ @Override
+ public void init(RendererCellReference cell) {
+ if (hasInit) {
+ init(helper.getConnectorWrapper(), getJsCell(cell));
+ }
+ }
+
+ private native void init(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ wrapper.init(cell);
+ }-*/;
+
+ private native void updateCellReference(
+ JavaScriptObject cellWrapper, CellReference<?> target)
+ /*-{
+ cellWrapper.target = target;
+ }-*/;
+
+ @Override
+ public void destroy(RendererCellReference cell) {
+ if (hasDestroy) {
+ destory(helper.getConnectorWrapper(), getJsCell(cell));
+ } else {
+ super.destroy(cell);
+ }
+ }
+
+ private native void destory(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ wrapper.destory(cell);
+ }-*/;
+
+ @Override
+ public boolean onActivate(CellReference<?> cell) {
+ if (hasOnActivate) {
+ return onActivate(helper.getConnectorWrapper(),
+ getJsCell(cell));
+ } else {
+ return super.onActivate(cell);
+ }
+ }
+
+ private native boolean onActivate(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ return !!wrapper.onActivate(cell);
+ }-*/;
+
+ @Override
+ public Collection<String> getConsumedEvents() {
+ if (hasGetConsumedEvents) {
+ JsArrayString events = getConsumedEvents(helper
+ .getConnectorWrapper());
+
+ ArrayList<String> list = new ArrayList<String>(
+ events.length());
+ for (int i = 0; i < events.length(); i++) {
+ list.add(events.get(i));
+ }
+ return list;
+ } else {
+ return super.getConsumedEvents();
+ }
+ }
+
+ private native JsArrayString getConsumedEvents(
+ JavaScriptObject wrapper)
+ /*-{
+ var rawEvents = wrapper.getConsumedEvents();
+ var events = [];
+ for(var i = 0; i < rawEvents.length; i++) {
+ events[i] = ""+rawEvents[i];
+ }
+ return events;
+ }-*/;
+
+ @Override
+ public boolean onBrowserEvent(CellReference<?> cell,
+ NativeEvent event) {
+ if (hasOnBrowserEvent) {
+ return onBrowserEvent(helper.getConnectorWrapper(),
+ getJsCell(cell), event);
+ } else {
+ return super.onBrowserEvent(cell, event);
+ }
+ }
+
+ private native boolean onBrowserEvent(JavaScriptObject wrapper,
+ JavaScriptObject cell, NativeEvent event)
+ /*-{
+ return !!wrapper.onBrowserEvent(cell, event);
+ }-*/;
+ };
+ }
+
+ @Override
++ public Object decode(JsonValue value) {
+ // Let the js logic decode the raw json that the server sent
+ return value;
+ }
+
+ @Override
+ public void onUnregister() {
+ super.onUnregister();
+ helper.onUnregister();
+ }
+
+ @Override
+ public JavaScriptConnectorHelper getJavascriptConnectorHelper() {
+ return helper;
+ }
+}
--- /dev/null
- setFocusOnLastElement(event);
+/*
+ * Copyright 2000-2014 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.client.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.event.dom.client.TouchStartEvent;
+import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.xhr.client.ReadyStateChangeHandler;
+import com.google.gwt.xhr.client.XMLHttpRequest;
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorMap;
+import com.vaadin.client.LayoutManager;
+import com.vaadin.client.MouseEventDetailsBuilder;
+import com.vaadin.client.Util;
+import com.vaadin.client.VConsole;
+import com.vaadin.client.ValueMap;
++import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.dd.DDUtil;
+import com.vaadin.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.client.ui.dd.VAcceptCallback;
+import com.vaadin.client.ui.dd.VDragAndDropManager;
+import com.vaadin.client.ui.dd.VDragEvent;
+import com.vaadin.client.ui.dd.VDropHandler;
+import com.vaadin.client.ui.dd.VHasDropHandler;
+import com.vaadin.client.ui.dd.VHtml5DragEvent;
+import com.vaadin.client.ui.dd.VHtml5File;
+import com.vaadin.client.ui.dd.VTransferable;
+import com.vaadin.shared.ui.dd.HorizontalDropLocation;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+
+/**
+ *
+ * Must have features pending:
+ *
+ * drop details: locations + sizes in document hierarchy up to wrapper
+ *
+ */
+public class VDragAndDropWrapper extends VCustomComponent implements
+ VHasDropHandler {
+
+ /**
+ * Minimum pixel delta is used to detect click from drag. #12838
+ */
+ private static final int MIN_PX_DELTA = 4;
+ private static final String CLASSNAME = "v-ddwrapper";
+ protected static final String DRAGGABLE = "draggable";
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean hasTooltip = false;
+ private int startX = 0;
+ private int startY = 0;
+
+ public VDragAndDropWrapper() {
+ super();
+ hookHtml5Events(getElement());
+ setStyleName(CLASSNAME);
+
+ addDomHandler(new MouseDownHandler() {
+
+ @Override
+ public void onMouseDown(final MouseDownEvent event) {
+ if (getConnector().isEnabled()
+ && event.getNativeEvent().getButton() == Event.BUTTON_LEFT
+ && startDrag(event.getNativeEvent())) {
+ event.preventDefault(); // prevent text selection
+ startX = event.getClientX();
+ startY = event.getClientY();
+ }
+ }
+ }, MouseDownEvent.getType());
+
+ addDomHandler(new MouseUpHandler() {
+
+ @Override
+ public void onMouseUp(final MouseUpEvent event) {
+ final int deltaX = Math.abs(event.getClientX() - startX);
+ final int deltaY = Math.abs(event.getClientY() - startY);
+ if ((deltaX + deltaY) < MIN_PX_DELTA) {
- private void setFocusOnLastElement(final MouseUpEvent event) {
- Element el = event.getRelativeElement();
- getLastChildElement(el).focus();
- }
-
- private Element getLastChildElement(Element el) {
- do {
- if (el == null) {
- break;
- }
- el = el.getFirstChildElement();
- } while (el.getFirstChildElement() != null);
- return el;
- }
-
++ Element clickedElement = WidgetUtil.getElementFromPoint(
++ event.getClientX(), event.getClientY());
++ clickedElement.focus();
+ }
+ }
+
- // visit server for possible
- // variable changes
- client.sendPendingVariableChanges();
+ }, MouseUpEvent.getType());
+
+ addDomHandler(new TouchStartHandler() {
+
+ @Override
+ public void onTouchStart(TouchStartEvent event) {
+ if (getConnector().isEnabled()
+ && startDrag(event.getNativeEvent())) {
+ /*
+ * Dont let eg. panel start scrolling.
+ */
+ event.stopPropagation();
+ }
+ }
+ }, TouchStartEvent.getType());
+
+ sinkEvents(Event.TOUCHEVENTS);
+ }
+
+ /**
+ * Starts a drag and drop operation from mousedown or touchstart event if
+ * required conditions are met.
+ *
+ * @param event
+ * @return true if the event was handled as a drag start event
+ */
+ private boolean startDrag(NativeEvent event) {
+ if (dragStartMode == WRAPPER || dragStartMode == COMPONENT
+ || dragStartMode == COMPONENT_OTHER) {
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(getConnector());
+
+ ComponentConnector paintable = Util.findPaintable(client,
+ Element.as(event.getEventTarget()));
+ Widget widget = paintable.getWidget();
+ transferable.setData("component", paintable);
+ VDragEvent dragEvent = VDragAndDropManager.get().startDrag(
+ transferable, event, true);
+
+ transferable.setData("mouseDown", MouseEventDetailsBuilder
+ .buildMouseEventDetails(event).serialize());
+
+ if (dragStartMode == WRAPPER) {
+ dragEvent.createDragImage(getElement(), true);
+ } else if (dragStartMode == COMPONENT_OTHER
+ && getDragImageWidget() != null) {
+ dragEvent.createDragImage(getDragImageWidget().getElement(),
+ true);
+ } else {
+ dragEvent.createDragImage(widget.getElement(), true);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ protected final static int NONE = 0;
+ protected final static int COMPONENT = 1;
+ protected final static int WRAPPER = 2;
+ protected final static int HTML5 = 3;
+ protected final static int COMPONENT_OTHER = 4;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int dragStartMode;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public ApplicationConnection client;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public VAbstractDropHandler dropHandler;
+
++ /** For internal use only. May be removed or replaced in the future. */
++ public UploadHandler uploadHandler;
++
+ private VDragEvent vaadinDragEvent;
+
+ int filecounter = 0;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public Map<String, String> fileIdToReceiver;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public ValueMap html5DataFlavors;
+
+ private Element dragStartElement;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void initDragStartMode() {
+ Element div = getElement();
+ if (dragStartMode == HTML5) {
+ if (dragStartElement == null) {
+ dragStartElement = getDragStartElement();
+ dragStartElement.setPropertyBoolean(DRAGGABLE, true);
+ VConsole.log("draggable = "
+ + dragStartElement.getPropertyBoolean(DRAGGABLE));
+ hookHtml5DragStart(dragStartElement);
+ VConsole.log("drag start listeners hooked.");
+ }
+ } else {
+ dragStartElement = null;
+ if (div.hasAttribute(DRAGGABLE)) {
+ div.removeAttribute(DRAGGABLE);
+ }
+ }
+ }
+
+ protected com.google.gwt.user.client.Element getDragStartElement() {
+ return getElement();
+ }
+
+ private boolean uploading;
+
+ private final ReadyStateChangeHandler readyStateChangeHandler = new ReadyStateChangeHandler() {
+
+ @Override
+ public void onReadyStateChange(XMLHttpRequest xhr) {
+ if (xhr.getReadyState() == XMLHttpRequest.DONE) {
++ // #19616 Notify the upload handler that the request is complete
++ // and let it poll the server for changes.
++ uploadHandler.uploadDone();
+ uploading = false;
+ startNextUpload();
+ xhr.clearOnReadyStateChange();
+ }
+ }
+ };
+ private Timer dragleavetimer;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void startNextUpload() {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ if (!uploading) {
+ if (fileIds.size() > 0) {
+
+ uploading = true;
+ final Integer fileId = fileIds.remove(0);
+ VHtml5File file = files.remove(0);
+ final String receiverUrl = client
+ .translateVaadinUri(fileIdToReceiver
+ .remove(fileId.toString()));
+ ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR
+ .create();
+ extendedXHR
+ .setOnReadyStateChange(readyStateChangeHandler);
+ extendedXHR.open("POST", receiverUrl);
+ extendedXHR.postFile(file);
+ }
+ }
+
+ }
+ });
+
+ }
+
+ public boolean html5DragStart(VHtml5DragEvent event) {
+ if (dragStartMode == HTML5) {
+ /*
+ * Populate html5 payload with dataflavors from the serverside
+ */
+ JsArrayString flavors = html5DataFlavors.getKeyArray();
+ for (int i = 0; i < flavors.length(); i++) {
+ String flavor = flavors.get(i);
+ event.setHtml5DataFlavor(flavor,
+ html5DataFlavors.getString(flavor));
+ }
+ event.setEffectAllowed("copy");
+ return true;
+ }
+ return false;
+ }
+
+ public boolean html5DragEnter(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+ try {
+ if (dragleavetimer != null) {
+ // returned quickly back to wrapper
+ dragleavetimer.cancel();
+ dragleavetimer = null;
+ }
+ if (VDragAndDropManager.get().getCurrentDropHandler() != getDropHandler()) {
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(getConnector());
+
+ vaadinDragEvent = VDragAndDropManager.get().startDrag(
+ transferable, event, false);
+ VDragAndDropManager.get().setCurrentDropHandler(
+ getDropHandler());
+ }
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+ }
+
+ public boolean html5DragLeave(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+
+ try {
+ dragleavetimer = new Timer() {
+
+ @Override
+ public void run() {
+ // Yes, dragleave happens before drop. Makes no sense to me.
+ // IMO shouldn't fire leave at all if drop happens (I guess
+ // this
+ // is what IE does).
+ // In Vaadin we fire it only if drop did not happen.
+ if (vaadinDragEvent != null
+ && VDragAndDropManager.get()
+ .getCurrentDropHandler() == getDropHandler()) {
+ VDragAndDropManager.get().interruptDrag();
+ }
+ }
+ };
+ dragleavetimer.schedule(350);
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+ }
+
+ public boolean html5DragOver(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+
+ if (dragleavetimer != null) {
+ // returned quickly back to wrapper
+ dragleavetimer.cancel();
+ dragleavetimer = null;
+ }
+
+ vaadinDragEvent.setCurrentGwtEvent(event);
+ getDropHandler().dragOver(vaadinDragEvent);
+
+ try {
+ String s = event.getEffectAllowed();
+ if ("all".equals(s) || s.contains("opy")) {
+ event.setDropEffect("copy");
+ } else {
+ event.setDropEffect(s);
+ }
+ } catch (Exception e) {
+ // IE10 throws exception here in getEffectAllowed, ignore it, let
+ // drop effect be whatever it is
+ }
+
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ }
+
+ public boolean html5DragDrop(VHtml5DragEvent event) {
+ if (dropHandler == null || !currentlyValid) {
+ return true;
+ }
+ try {
+
+ VTransferable transferable = vaadinDragEvent.getTransferable();
+
+ JsArrayString types = event.getTypes();
+ for (int i = 0; i < types.length(); i++) {
+ String type = types.get(i);
+ if (isAcceptedType(type)) {
+ String data = event.getDataAsText(type);
+ if (data != null) {
+ transferable.setData(type, data);
+ }
+ }
+ }
+
+ int fileCount = event.getFileCount();
+ if (fileCount > 0) {
+ transferable.setData("filecount", fileCount);
+ for (int i = 0; i < fileCount; i++) {
+ final int fileId = filecounter++;
+ final VHtml5File file = event.getFile(i);
+ VConsole.log("Preparing to upload file " + file.getName()
+ + " with id " + fileId);
+ transferable.setData("fi" + i, "" + fileId);
+ transferable.setData("fn" + i, file.getName());
+ transferable.setData("ft" + i, file.getType());
+ transferable.setData("fs" + i, file.getSize());
+ queueFilePost(fileId, file);
+ }
+
+ }
+
+ VDragAndDropManager.get().endDrag();
+ vaadinDragEvent = null;
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+
+ }
+
+ protected String[] acceptedTypes = new String[] { "Text", "Url",
+ "text/html", "text/plain", "text/rtf" };
+
+ private boolean isAcceptedType(String type) {
+ for (String t : acceptedTypes) {
+ if (t.equals(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static class ExtendedXHR extends XMLHttpRequest {
+
+ protected ExtendedXHR() {
+ }
+
+ public final native void postFile(VHtml5File file)
+ /*-{
+
+ this.setRequestHeader('Content-Type', 'multipart/form-data');
+ // Seems like IE10 will loose the file if we don't keep a reference to it...
+ this.fileBeingUploaded = file;
+
+ this.send(file);
+ }-*/;
+
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public List<Integer> fileIds = new ArrayList<Integer>();
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public List<VHtml5File> files = new ArrayList<VHtml5File>();
+
+ private void queueFilePost(final int fileId, final VHtml5File file) {
+ fileIds.add(fileId);
+ files.add(file);
+ }
+
+ @Override
+ public VDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ protected VerticalDropLocation verticalDropLocation;
+ protected HorizontalDropLocation horizontalDropLocation;
+ private VerticalDropLocation emphasizedVDrop;
+ private HorizontalDropLocation emphasizedHDrop;
+
+ /**
+ * Flag used by html5 dd
+ */
+ private boolean currentlyValid;
+ private Widget dragImageWidget;
+
+ private static final String OVER_STYLE = "v-ddwrapper-over";
+
+ public class CustomDropHandler extends VAbstractDropHandler {
+
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ if (!getConnector().isEnabled()) {
+ return;
+ }
+ updateDropDetails(drag);
+ currentlyValid = false;
+ super.dragEnter(drag);
+ }
+
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ deEmphasis(true);
+ dragleavetimer = null;
+ }
+
+ @Override
+ public void dragOver(final VDragEvent drag) {
+ if (!getConnector().isEnabled()) {
+ return;
+ }
+ boolean detailsChanged = updateDropDetails(drag);
+ if (detailsChanged) {
+ currentlyValid = false;
+ validate(new VAcceptCallback() {
+
+ @Override
+ public void accepted(VDragEvent event) {
+ dragAccepted(drag);
+ }
+ }, drag);
+ }
+ }
+
+ @Override
+ public boolean drop(VDragEvent drag) {
+ if (!getConnector().isEnabled()) {
+ return false;
+ }
+ deEmphasis(true);
+
+ Map<String, Object> dd = drag.getDropDetails();
+
+ // this is absolute layout based, and we may want to set
+ // component
+ // relatively to where the drag ended.
+ // need to add current location of the drop area
+
+ int absoluteLeft = getAbsoluteLeft();
+ int absoluteTop = getAbsoluteTop();
+
+ dd.put("absoluteLeft", absoluteLeft);
+ dd.put("absoluteTop", absoluteTop);
+
+ if (verticalDropLocation != null) {
+ dd.put("verticalLocation", verticalDropLocation.toString());
+ dd.put("horizontalLocation", horizontalDropLocation.toString());
+ }
+
+ return super.drop(drag);
+ }
+
+ @Override
+ protected void dragAccepted(VDragEvent drag) {
+ if (!getConnector().isEnabled()) {
+ return;
+ }
+ currentlyValid = true;
+ emphasis(drag);
+ }
+
+ @Override
+ public ComponentConnector getConnector() {
+ return VDragAndDropWrapper.this.getConnector();
+ }
+
+ @Override
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ }
+
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(this);
+ }
+
+ /**
+ * @deprecated As of 7.2, call or override
+ * {@link #hookHtml5DragStart(Element)} instead
+ */
+ @Deprecated
+ protected native void hookHtml5DragStart(
+ com.google.gwt.user.client.Element el)
+ /*-{
+ var me = this;
+ el.addEventListener("dragstart", $entry(function(ev) {
+ return me.@com.vaadin.client.ui.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+ }-*/;
+
+ /**
+ * @since 7.2
+ */
+ protected void hookHtml5DragStart(Element el) {
+ hookHtml5DragStart(DOM.asOld(el));
+ }
+
+ /**
+ * Prototype code, memory leak risk.
+ *
+ * @param el
+ * @deprecated As of 7.2, call or override {@link #hookHtml5Events(Element)}
+ * instead
+ */
+ @Deprecated
+ protected native void hookHtml5Events(com.google.gwt.user.client.Element el)
+ /*-{
+ var me = this;
+
+ el.addEventListener("dragenter", $entry(function(ev) {
+ return me.@com.vaadin.client.ui.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+
+ el.addEventListener("dragleave", $entry(function(ev) {
+ return me.@com.vaadin.client.ui.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+
+ el.addEventListener("dragover", $entry(function(ev) {
+ return me.@com.vaadin.client.ui.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+
+ el.addEventListener("drop", $entry(function(ev) {
+ return me.@com.vaadin.client.ui.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+ }-*/;
+
+ /**
+ * Prototype code, memory leak risk.
+ *
+ * @param el
+ *
+ * @since 7.2
+ */
+ protected void hookHtml5Events(Element el) {
+ hookHtml5Events(DOM.asOld(el));
+ }
+
+ public boolean updateDropDetails(VDragEvent drag) {
+ VerticalDropLocation oldVL = verticalDropLocation;
+ verticalDropLocation = DDUtil.getVerticalDropLocation(getElement(),
+ drag.getCurrentGwtEvent(), 0.2);
+ drag.getDropDetails().put("verticalLocation",
+ verticalDropLocation.toString());
+ HorizontalDropLocation oldHL = horizontalDropLocation;
+ horizontalDropLocation = DDUtil.getHorizontalDropLocation(getElement(),
+ drag.getCurrentGwtEvent(), 0.2);
+ drag.getDropDetails().put("horizontalLocation",
+ horizontalDropLocation.toString());
+ if (oldHL != horizontalDropLocation || oldVL != verticalDropLocation) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected void deEmphasis(boolean doLayout) {
+ if (emphasizedVDrop != null) {
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + emphasizedVDrop.toString().toLowerCase(), false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + emphasizedHDrop.toString().toLowerCase(), false);
+ }
+ if (doLayout) {
+ notifySizePotentiallyChanged();
+ }
+ }
+
+ private void notifySizePotentiallyChanged() {
+ LayoutManager.get(client).setNeedsMeasure(getConnector());
+ }
+
+ protected void emphasis(VDragEvent drag) {
+ deEmphasis(false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, true);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + verticalDropLocation.toString().toLowerCase(), true);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + horizontalDropLocation.toString().toLowerCase(), true);
+ emphasizedVDrop = verticalDropLocation;
+ emphasizedHDrop = horizontalDropLocation;
+
+ // TODO build (to be an example) an emphasis mode where drag image
+ // is fitted before or after the content
+ notifySizePotentiallyChanged();
+ }
+
+ /**
+ * Set the widget that will be used as the drag image when using
+ * DragStartMode {@link COMPONENT_OTHER} .
+ *
+ * @param widget
+ */
+ public void setDragAndDropWidget(Widget widget) {
+ dragImageWidget = widget;
+ }
+
+ /**
+ * @return the widget used as drag image. Returns <code>null</code> if no
+ * widget is set.
+ */
+ public Widget getDragImageWidget() {
+ return dragImageWidget;
+ }
+
++ /**
++ * Internal client side interface used by the connector and the widget for
++ * the drag and drop wrapper to signal the completion of an HTML5 file
++ * upload.
++ *
++ * @since 7.6.4
++ */
++ public interface UploadHandler {
++
++ public void uploadDone();
++
++ }
++
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.client.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.aria.client.Roles;
+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.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.i18n.client.HasDirection.Direction;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+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.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ComputedStyle;
+import com.vaadin.client.ConnectorMap;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.Focusable;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.VConsole;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.aria.AriaHelper;
+import com.vaadin.client.ui.aria.HandlesAriaCaption;
+import com.vaadin.client.ui.aria.HandlesAriaInvalid;
+import com.vaadin.client.ui.aria.HandlesAriaRequired;
+import com.vaadin.client.ui.menubar.MenuBar;
+import com.vaadin.client.ui.menubar.MenuItem;
+import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.EventId;
+import com.vaadin.shared.ui.ComponentStateUtil;
+import com.vaadin.shared.ui.combobox.FilteringMode;
+import com.vaadin.shared.util.SharedUtil;
+
+/**
+ * Client side implementation of the Select component.
+ *
+ * TODO needs major refactoring (to be extensible etc)
+ */
+@SuppressWarnings("deprecation")
+public class VFilterSelect extends Composite implements Field, KeyDownHandler,
+ KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable,
+ SubPartAware, HandlesAriaCaption, HandlesAriaInvalid,
+ HandlesAriaRequired, DeferredWorker {
+
+ /**
+ * Represents a suggestion in the suggestion popup box
+ */
+ public class FilterSelectSuggestion implements Suggestion, Command {
+
+ private final String key;
+ private final String caption;
+ private String untranslatedIconUri;
+ private String style;
+
+ /**
+ * Constructor
+ *
+ * @param uidl
+ * The UIDL recieved from the server
+ */
+ public FilterSelectSuggestion(UIDL uidl) {
+ key = uidl.getStringAttribute("key");
+ caption = uidl.getStringAttribute("caption");
+ style = uidl.getStringAttribute("style");
+
+ if (uidl.hasAttribute("icon")) {
+ untranslatedIconUri = uidl.getStringAttribute("icon");
+ }
+ }
+
+ /**
+ * Gets the visible row in the popup as a HTML string. The string
+ * contains an image tag with the rows icon (if an icon has been
+ * specified) and the caption of the item
+ */
+
+ @Override
+ public String getDisplayString() {
+ final StringBuffer sb = new StringBuffer();
+ final Icon icon = client.getIcon(client
+ .translateVaadinUri(untranslatedIconUri));
+ if (icon != null) {
+ sb.append(icon.getElement().getString());
+ }
+ String content;
+ if ("".equals(caption)) {
+ // Ensure that empty options use the same height as other
+ // options and are not collapsed (#7506)
+ content = " ";
+ } else {
+ content = WidgetUtil.escapeHTML(caption);
+ }
+ sb.append("<span>" + content + "</span>");
+ return sb.toString();
+ }
+
+ /**
+ * Get a string that represents this item. This is used in the text box.
+ */
+
+ @Override
+ public String getReplacementString() {
+ return caption;
+ }
+
+ /**
+ * Get the option key which represents the item on the server side.
+ *
+ * @return The key of the item
+ */
+ public String getOptionKey() {
+ return key;
+ }
+
+ /**
+ * Get the URI of the icon. Used when constructing the displayed option.
+ *
+ * @return
+ */
+ public String getIconUri() {
+ return client.translateVaadinUri(untranslatedIconUri);
+ }
+
+ /**
+ * Gets the style set for this suggestion item. Styles are typically set
+ * by a server-side {@link com.vaadin.ui.ComboBox.ItemStyleGenerator}.
+ * The returned style is prefixed by <code>v-filterselect-item-</code>.
+ *
+ * @since 7.5.6
+ * @return the style name to use, or <code>null</code> to not apply any
+ * custom style.
+ */
+ public String getStyle() {
+ return style;
+ }
+
+ /**
+ * Executes a selection of this item.
+ */
+
+ @Override
+ public void execute() {
+ onSuggestionSelected(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof FilterSelectSuggestion)) {
+ return false;
+ }
+ FilterSelectSuggestion other = (FilterSelectSuggestion) obj;
+ if ((key == null && other.key != null)
+ || (key != null && !key.equals(other.key))) {
+ return false;
+ }
+ if ((caption == null && other.caption != null)
+ || (caption != null && !caption.equals(other.caption))) {
+ return false;
+ }
+ if (!SharedUtil.equals(untranslatedIconUri,
+ other.untranslatedIconUri)) {
+ return false;
+ }
+ if (!SharedUtil.equals(style, other.style)) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /** An inner class that handles all logic related to mouse wheel. */
+ private class MouseWheeler extends JsniMousewheelHandler {
+
+ public MouseWheeler() {
+ super(VFilterSelect.this);
+ }
+
+ @Override
+ protected native JavaScriptObject createMousewheelListenerFunction(
+ Widget widget)
+ /*-{
+ return $entry(function(e) {
+ var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
+ var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
+
+ // IE8 has only delta y
+ if (isNaN(deltaY)) {
+ deltaY = -0.5*e.wheelDelta;
+ }
+
+ @com.vaadin.client.ui.VFilterSelect.JsniUtil::moveScrollFromEvent(*)(widget, deltaX, deltaY, e);
+ });
+ }-*/;
+
+ }
+
+ /**
+ * A utility class that contains utility methods that are usually called
+ * from JSNI.
+ * <p>
+ * The methods are moved in this class to minimize the amount of JSNI code
+ * as much as feasible.
+ */
+ static class JsniUtil {
+ public static void moveScrollFromEvent(final Widget widget,
+ final double deltaX, final double deltaY,
+ final NativeEvent event) {
+
+ if (!Double.isNaN(deltaY)) {
+ ((VFilterSelect) widget).suggestionPopup.scroll(deltaY);
+ }
+ }
+ }
+
+ /**
+ * Represents the popup box with the selection options. Wraps a suggestion
+ * menu.
+ */
+ public class SuggestionPopup extends VOverlay implements PositionCallback,
+ CloseHandler<PopupPanel> {
+
+ private static final int Z_INDEX = 30000;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final SuggestionMenu menu;
+
+ private final Element up = DOM.createDiv();
+ private final Element down = DOM.createDiv();
+ private final Element status = DOM.createDiv();
+
+ private boolean isPagingEnabled = true;
+
+ private long lastAutoClosed;
+
+ private int popupOuterPadding = -1;
+
+ private int topPosition;
+
+ private final MouseWheeler mouseWheeler = new MouseWheeler();
+
+ /**
+ * Default constructor
+ */
+ SuggestionPopup() {
+ super(true, false, true);
+ debug("VFS.SP: constructor()");
+ setOwner(VFilterSelect.this);
+ menu = new SuggestionMenu();
+ setWidget(menu);
+
+ getElement().getStyle().setZIndex(Z_INDEX);
+
+ final Element root = getContainerElement();
+
+ up.setInnerHTML("<span>Prev</span>");
+ DOM.sinkEvents(up, Event.ONCLICK);
+
+ down.setInnerHTML("<span>Next</span>");
+ DOM.sinkEvents(down, Event.ONCLICK);
+
+ root.insertFirst(up);
+ root.appendChild(down);
+ root.appendChild(status);
+
+ DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL);
+ addCloseHandler(this);
+
+ Roles.getListRole().set(getElement());
+
+ setPreviewingAllNativeEvents(true);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ mouseWheeler.attachMousewheelListener(getElement());
+ }
+
+ @Override
+ protected void onUnload() {
+ mouseWheeler.detachMousewheelListener(getElement());
+ super.onUnload();
+ }
+
+ /**
+ * Shows the popup where the user can see the filtered options
+ *
+ * @param currentSuggestions
+ * The filtered suggestions
+ * @param currentPage
+ * The current page number
+ * @param totalSuggestions
+ * The total amount of suggestions
+ */
+ public void showSuggestions(
+ final Collection<FilterSelectSuggestion> currentSuggestions,
+ final int currentPage, final int totalSuggestions) {
+
+ debug("VFS.SP: showSuggestions(" + currentSuggestions + ", "
+ + currentPage + ", " + totalSuggestions + ")");
+
+ /*
+ * We need to defer the opening of the popup so that the parent DOM
+ * has stabilized so we can calculate an absolute top and left
+ * correctly. This issue manifests when a Combobox is placed in
+ * another popupView which also needs to calculate the absoluteTop()
+ * to position itself. #9768
+ *
+ * After deferring the showSuggestions method, a problem with
+ * navigating in the combo box occurs. Because of that the method
+ * navigateItemAfterPageChange in ComboBoxConnector class, which
+ * navigates to the exact item after page was changed also was
+ * marked as deferred. #11333
+ */
+ final SuggestionPopup popup = this;
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ // Add TT anchor point
+ getElement().setId("VAADIN_COMBOBOX_OPTIONLIST");
+
+ menu.setSuggestions(currentSuggestions);
+ final int x = VFilterSelect.this.getAbsoluteLeft();
+
+ topPosition = tb.getAbsoluteTop();
+ topPosition += tb.getOffsetHeight();
+
+ setPopupPosition(x, topPosition);
+
+ int nullOffset = (nullSelectionAllowed
+ && "".equals(lastFilter) ? 1 : 0);
+ boolean firstPage = (currentPage == 0);
+ final int first = currentPage * pageLength + 1
+ - (firstPage ? 0 : nullOffset);
+ final int last = first
+ + currentSuggestions.size()
+ - 1
+ - (firstPage && "".equals(lastFilter) ? nullOffset
+ : 0);
+ final int matches = totalSuggestions - nullOffset;
+ if (last > 0) {
+ // nullsel not counted, as requested by user
+ status.setInnerText((matches == 0 ? 0 : first) + "-"
+ + last + "/" + matches);
+ } else {
+ status.setInnerText("");
+ }
+ // We don't need to show arrows or statusbar if there is
+ // only one page
+ if (totalSuggestions <= pageLength || pageLength == 0) {
+ setPagingEnabled(false);
+ } else {
+ setPagingEnabled(true);
+ }
+ setPrevButtonActive(first > 1);
+ setNextButtonActive(last < matches);
+
+ // clear previously fixed width
+ menu.setWidth("");
+ menu.getElement().getFirstChildElement().getStyle()
+ .clearWidth();
+
+ setPopupPositionAndShow(popup);
+ // Fix for #14173
+ // IE9 and IE10 have a bug, when resize an a element with
+ // box-shadow.
+ // IE9 and IE10 need explicit update to remove extra
+ // box-shadows
+ if (BrowserInfo.get().isIE9() || BrowserInfo.get().isIE10()) {
+ forceReflow();
+ }
+ }
+ });
+ }
+
+ /**
+ * Should the next page button be visible to the user?
+ *
+ * @param active
+ */
+ private void setNextButtonActive(boolean active) {
+ if (enableDebug) {
+ debug("VFS.SP: setNextButtonActive(" + active + ")");
+ }
+ if (active) {
+ DOM.sinkEvents(down, Event.ONCLICK);
+ down.setClassName(VFilterSelect.this.getStylePrimaryName()
+ + "-nextpage");
+ } else {
+ DOM.sinkEvents(down, 0);
+ down.setClassName(VFilterSelect.this.getStylePrimaryName()
+ + "-nextpage-off");
+ }
+ }
+
+ /**
+ * Should the previous page button be visible to the user
+ *
+ * @param active
+ */
+ private void setPrevButtonActive(boolean active) {
+ if (enableDebug) {
+ debug("VFS.SP: setPrevButtonActive(" + active + ")");
+ }
+
+ if (active) {
+ DOM.sinkEvents(up, Event.ONCLICK);
+ up.setClassName(VFilterSelect.this.getStylePrimaryName()
+ + "-prevpage");
+ } else {
+ DOM.sinkEvents(up, 0);
+ up.setClassName(VFilterSelect.this.getStylePrimaryName()
+ + "-prevpage-off");
+ }
+
+ }
+
+ /**
+ * Selects the next item in the filtered selections
+ */
+ public void selectNextItem() {
+ debug("VFS.SP: selectNextItem()");
+
+ final int index = menu.getSelectedIndex() + 1;
+ if (menu.getItems().size() > index) {
+ selectItem(menu.getItems().get(index));
+
+ } else {
+ selectNextPage();
+ }
+ }
+
+ /**
+ * Selects the previous item in the filtered selections
+ */
+ public void selectPrevItem() {
+ debug("VFS.SP: selectPrevItem()");
+
+ final int index = menu.getSelectedIndex() - 1;
+ if (index > -1) {
+ selectItem(menu.getItems().get(index));
+
+ } else if (index == -1) {
+ selectPrevPage();
+
+ } else {
+ if (!menu.getItems().isEmpty()) {
+ selectLastItem();
+ }
+ }
+ }
+
+ /**
+ * Select the first item of the suggestions list popup.
+ *
+ * @since 7.2.6
+ */
+ public void selectFirstItem() {
+ debug("VFS.SP: selectFirstItem()");
+ selectItem(menu.getFirstItem());
+ }
+
+ /**
+ * Select the last item of the suggestions list popup.
+ *
+ * @since 7.2.6
+ */
+ public void selectLastItem() {
+ debug("VFS.SP: selectLastItem()");
+ selectItem(menu.getLastItem());
+ }
+
+ /*
+ * Sets the selected item in the popup menu.
+ */
+ private void selectItem(final MenuItem newSelectedItem) {
+ menu.selectItem(newSelectedItem);
+
+ // Set the icon.
+ FilterSelectSuggestion suggestion = (FilterSelectSuggestion) newSelectedItem
+ .getCommand();
+ setSelectedItemIcon(suggestion.getIconUri());
+
+ // Set the text.
+ setText(suggestion.getReplacementString());
+
+ }
+
+ /*
+ * Using a timer to scroll up or down the pages so when we receive lots
+ * of consecutive mouse wheel events the pages does not flicker.
+ */
+ private LazyPageScroller lazyPageScroller = new LazyPageScroller();
+
+ private class LazyPageScroller extends Timer {
+ private int pagesToScroll = 0;
+
+ @Override
+ public void run() {
+ debug("VFS.SP.LPS: run()");
+ if (pagesToScroll != 0) {
+ if (!waitingForFilteringResponse) {
+ /*
+ * Avoid scrolling while we are waiting for a response
+ * because otherwise the waiting flag will be reset in
+ * the first response and the second response will be
+ * ignored, causing an empty popup...
+ *
+ * As long as the scrolling delay is suitable
+ * double/triple clicks will work by scrolling two or
+ * three pages at a time and this should not be a
+ * problem.
+ */
+ filterOptions(currentPage + pagesToScroll, lastFilter);
+ }
+ pagesToScroll = 0;
+ }
+ }
+
+ public void scrollUp() {
+ debug("VFS.SP.LPS: scrollUp()");
+ if (pageLength > 0 && currentPage + pagesToScroll > 0) {
+ pagesToScroll--;
+ cancel();
+ schedule(200);
+ }
+ }
+
+ public void scrollDown() {
+ debug("VFS.SP.LPS: scrollDown()");
+ if (pageLength > 0
+ && totalMatches > (currentPage + pagesToScroll + 1)
+ * pageLength) {
+ pagesToScroll++;
+ cancel();
+ schedule(200);
+ }
+ }
+ }
+
+ private void scroll(double deltaY) {
+ boolean scrollActive = menu.isScrollActive();
+
+ debug("VFS.SP: scroll() scrollActive: " + scrollActive);
+
+ if (!scrollActive) {
+ if (deltaY > 0d) {
+ lazyPageScroller.scrollDown();
+ } else {
+ lazyPageScroller.scrollUp();
+ }
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ debug("VFS.SP: onBrowserEvent()");
+
+ if (event.getTypeInt() == Event.ONCLICK) {
+ final Element target = DOM.eventGetTarget(event);
+ if (target == up || target == DOM.getChild(up, 0)) {
+ lazyPageScroller.scrollUp();
+ } else if (target == down || target == DOM.getChild(down, 0)) {
+ lazyPageScroller.scrollDown();
+ }
+
+ }
+
+ /*
+ * Prevent the keyboard focus from leaving the textfield by
+ * preventing the default behaviour of the browser. Fixes #4285.
+ */
+ handleMouseDownEvent(event);
+ }
+
+ /**
+ * Should paging be enabled. If paging is enabled then only a certain
+ * amount of items are visible at a time and a scrollbar or buttons are
+ * visible to change page. If paging is turned of then all options are
+ * rendered into the popup menu.
+ *
+ * @param paging
+ * Should the paging be turned on?
+ */
+ public void setPagingEnabled(boolean paging) {
+ debug("VFS.SP: setPagingEnabled(" + paging + ")");
+ if (isPagingEnabled == paging) {
+ return;
+ }
+ if (paging) {
+ down.getStyle().clearDisplay();
+ up.getStyle().clearDisplay();
+ status.getStyle().clearDisplay();
+ } else {
+ down.getStyle().setDisplay(Display.NONE);
+ up.getStyle().setDisplay(Display.NONE);
+ status.getStyle().setDisplay(Display.NONE);
+ }
+ isPagingEnabled = paging;
+ }
+
+ @Override
+ public void setPosition(int offsetWidth, int offsetHeight) {
+ debug("VFS.SP: setPosition(" + offsetWidth + ", " + offsetHeight
+ + ")");
+
+ int top = topPosition;
+ int left = getPopupLeft();
+
+ // reset menu size and retrieve its "natural" size
+ menu.setHeight("");
+ if (currentPage > 0 && !hasNextPage()) {
+ // fix height to avoid height change when getting to last page
+ menu.fixHeightTo(pageLength);
+ }
+
+ final int desiredHeight = offsetHeight = getOffsetHeight();
+ final int desiredWidth = getMainWidth();
+
+ debug("VFS.SP: desired[" + desiredWidth + ", " + desiredHeight
+ + "]");
+
+ Element menuFirstChild = menu.getElement().getFirstChildElement();
+ final int naturalMenuWidth = WidgetUtil
+ .getRequiredWidth(menuFirstChild);
+
+ if (popupOuterPadding == -1) {
+ popupOuterPadding = WidgetUtil
+ .measureHorizontalPaddingAndBorder(getElement(), 2);
+ }
+
+ if (naturalMenuWidth < desiredWidth) {
+ menu.setWidth((desiredWidth - popupOuterPadding) + "px");
+ menuFirstChild.getStyle().setWidth(100, Unit.PCT);
+ }
+
+ if (BrowserInfo.get().isIE()
+ && BrowserInfo.get().getBrowserMajorVersion() < 11) {
+ // Must take margin,border,padding manually into account for
+ // menu element as we measure the element child and set width to
+ // the element parent
+ double naturalMenuOuterWidth = WidgetUtil
+ .getRequiredWidthDouble(menuFirstChild)
+ + getMarginBorderPaddingWidth(menu.getElement());
+
+ /*
+ * IE requires us to specify the width for the container
+ * element. Otherwise it will be 100% wide
+ */
+ double rootWidth = Math.max(desiredWidth - popupOuterPadding,
+ naturalMenuOuterWidth);
+ getContainerElement().getStyle().setWidth(rootWidth, Unit.PX);
+ }
+
+ final int vfsHeight = VFilterSelect.this.getOffsetHeight();
+ final int spaceAvailableAbove = top - vfsHeight;
+ final int spaceAvailableBelow = Window.getClientHeight() - top;
+ if (spaceAvailableBelow < offsetHeight
+ && spaceAvailableBelow < spaceAvailableAbove) {
+ // popup on top of input instead
+ top -= offsetHeight + vfsHeight;
+ if (top < 0) {
+ offsetHeight += top;
+ top = 0;
+ }
+ } else {
+ offsetHeight = Math.min(offsetHeight, spaceAvailableBelow);
+ }
+
+ // fetch real width (mac FF bugs here due GWT popups overflow:auto )
+ offsetWidth = menuFirstChild.getOffsetWidth();
+
+ if (offsetHeight < desiredHeight) {
+ int menuHeight = offsetHeight;
+ if (isPagingEnabled) {
+ menuHeight -= up.getOffsetHeight() + down.getOffsetHeight()
+ + status.getOffsetHeight();
+ } else {
+ final ComputedStyle s = new ComputedStyle(menu.getElement());
+ menuHeight -= s.getIntProperty("marginBottom")
+ + s.getIntProperty("marginTop");
+ }
+
+ // If the available page height is really tiny then this will be
+ // negative and an exception will be thrown on setHeight.
+ int menuElementHeight = menu.getItemOffsetHeight();
+ if (menuHeight < menuElementHeight) {
+ menuHeight = menuElementHeight;
+ }
+
+ menu.setHeight(menuHeight + "px");
+
+ final int naturalMenuWidthPlusScrollBar = naturalMenuWidth
+ + WidgetUtil.getNativeScrollbarSize();
+ if (offsetWidth < naturalMenuWidthPlusScrollBar) {
+ menu.setWidth(naturalMenuWidthPlusScrollBar + "px");
+ }
+ }
+
+ if (offsetWidth + left > Window.getClientWidth()) {
+ left = VFilterSelect.this.getAbsoluteLeft()
+ + VFilterSelect.this.getOffsetWidth() - offsetWidth;
+ if (left < 0) {
+ left = 0;
+ menu.setWidth(Window.getClientWidth() + "px");
+ }
+ }
+
+ setPopupPosition(left, top);
+ menu.scrollSelectionIntoView();
+ }
+
+ /**
+ * Was the popup just closed?
+ *
+ * @return true if popup was just closed
+ */
+ public boolean isJustClosed() {
+ debug("VFS.SP: justClosed()");
+ final long now = (new Date()).getTime();
+ return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google
+ * .gwt.event.logical.shared.CloseEvent)
+ */
+
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ if (enableDebug) {
+ debug("VFS.SP: onClose(" + event.isAutoClosed() + ")");
+ }
+ if (event.isAutoClosed()) {
+ lastAutoClosed = (new Date()).getTime();
+ }
+ }
+
+ /**
+ * Updates style names in suggestion popup to help theme building.
+ *
+ * @param uidl
+ * UIDL for the whole combo box
+ * @param componentState
+ * shared state of the combo box
+ */
+ public void updateStyleNames(UIDL uidl,
+ AbstractComponentState componentState) {
+ debug("VFS.SP: updateStyleNames()");
+ setStyleName(VFilterSelect.this.getStylePrimaryName()
+ + "-suggestpopup");
+ menu.setStyleName(VFilterSelect.this.getStylePrimaryName()
+ + "-suggestmenu");
+ status.setClassName(VFilterSelect.this.getStylePrimaryName()
+ + "-status");
+ if (ComponentStateUtil.hasStyles(componentState)) {
+ for (String style : componentState.styles) {
+ if (!"".equals(style)) {
+ addStyleDependentName(style);
+ }
+ }
+ }
+ }
+
+ }
+
+ /**
+ * The menu where the suggestions are rendered
+ */
+ public class SuggestionMenu extends MenuBar implements SubPartAware,
+ LoadHandler {
+
+ private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(
+ 100, new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ debug("VFS.SM: delayedImageLoadExecutioner()");
+ if (suggestionPopup.isVisible()
+ && suggestionPopup.isAttached()) {
+ setWidth("");
+ getElement().getFirstChildElement().getStyle()
+ .clearWidth();
+ suggestionPopup
+ .setPopupPositionAndShow(suggestionPopup);
+ }
+
+ }
+ });
+
+ /**
+ * Default constructor
+ */
+ SuggestionMenu() {
+ super(true);
+ debug("VFS.SM: constructor()");
+ addDomHandler(this, LoadEvent.getType());
+
+ setScrollEnabled(true);
+ }
+
+ /**
+ * Fixes menus height to use same space as full page would use. Needed
+ * to avoid height changes when quickly "scrolling" to last page.
+ */
+ public void fixHeightTo(int pageItemsCount) {
+ setHeight(getPreferredHeight(pageItemsCount));
+ }
+
+ /*
+ * Gets the preferred height of the menu including pageItemsCount items.
+ */
+ String getPreferredHeight(int pageItemsCount) {
+ if (currentSuggestions.size() > 0) {
+ final int pixels = (getPreferredHeight() / currentSuggestions
+ .size()) * pageItemsCount;
+ return pixels + "px";
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Sets the suggestions rendered in the menu
+ *
+ * @param suggestions
+ * The suggestions to be rendered in the menu
+ */
+ public void setSuggestions(
+ Collection<FilterSelectSuggestion> suggestions) {
+ if (enableDebug) {
+ debug("VFS.SM: setSuggestions(" + suggestions + ")");
+ }
+
+ clearItems();
+ final Iterator<FilterSelectSuggestion> it = suggestions.iterator();
+ boolean isFirstIteration = true;
+ while (it.hasNext()) {
+ final FilterSelectSuggestion s = it.next();
+ final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
+ String style = s.getStyle();
+ if (style != null) {
+ mi.addStyleName("v-filterselect-item-" + style);
+ }
+ Roles.getListitemRole().set(mi.getElement());
+
+ WidgetUtil.sinkOnloadForImages(mi.getElement());
+
+ this.addItem(mi);
+
+ // By default, first item on the list is always highlighted,
+ // unless adding new items is allowed.
+ if (isFirstIteration && !allowNewItem) {
+ selectItem(mi);
+ }
+
+ // If the filter matches the current selection, highlight that
+ // instead of the first item.
+ if (tb.getText().equals(s.getReplacementString())
+ && s == currentSuggestion) {
+ selectItem(mi);
+ }
+
+ isFirstIteration = false;
+ }
+ }
+
+ /**
+ * Send the current selection to the server. Triggered when a selection
+ * is made or on a blur event.
+ */
+ public void doSelectedItemAction() {
+ debug("VFS.SM: doSelectedItemAction()");
+ // do not send a value change event if null was and stays selected
+ final String enteredItemValue = tb.getText();
+ if (nullSelectionAllowed && "".equals(enteredItemValue)
+ && selectedOptionKey != null
+ && !"".equals(selectedOptionKey)) {
+ if (nullSelectItem) {
+ reset();
+ return;
+ }
+ // null is not visible on pages != 0, and not visible when
+ // filtering: handle separately
+ client.updateVariable(paintableId, "filter", "", false);
+ client.updateVariable(paintableId, "page", 0, false);
+ client.updateVariable(paintableId, "selected", new String[] {},
+ immediate);
+ afterUpdateClientVariables();
+
+ suggestionPopup.hide();
+ return;
+ }
+
+ updateSelectionWhenReponseIsReceived = waitingForFilteringResponse;
+ if (!waitingForFilteringResponse) {
+ doPostFilterSelectedItemAction();
+ }
+ }
+
+ /**
+ * Triggered after a selection has been made
+ */
+ public void doPostFilterSelectedItemAction() {
+ debug("VFS.SM: doPostFilterSelectedItemAction()");
+ final MenuItem item = getSelectedItem();
+ final String enteredItemValue = tb.getText();
+
+ updateSelectionWhenReponseIsReceived = false;
+
+ // check for exact match in menu
+ int p = getItems().size();
+ if (p > 0) {
+ for (int i = 0; i < p; i++) {
+ final MenuItem potentialExactMatch = getItems().get(i);
+ if (potentialExactMatch.getText().equals(enteredItemValue)) {
+ selectItem(potentialExactMatch);
+ // do not send a value change event if null was and
+ // stays selected
+ if (!"".equals(enteredItemValue)
+ || (selectedOptionKey != null && !""
+ .equals(selectedOptionKey))) {
+ doItemAction(potentialExactMatch, true);
+ }
+ suggestionPopup.hide();
+ return;
+ }
+ }
+ }
+ if (allowNewItem) {
+
+ if (!prompting && !enteredItemValue.equals(lastNewItemString)) {
+ /*
+ * Store last sent new item string to avoid double sends
+ */
+ lastNewItemString = enteredItemValue;
+ client.updateVariable(paintableId, "newitem",
+ enteredItemValue, immediate);
+ afterUpdateClientVariables();
+ }
+ } else if (item != null
+ && !"".equals(lastFilter)
+ && (filteringmode == FilteringMode.CONTAINS ? item
+ .getText().toLowerCase()
+ .contains(lastFilter.toLowerCase()) : item
+ .getText().toLowerCase()
+ .startsWith(lastFilter.toLowerCase()))) {
+ doItemAction(item, true);
+ } else {
+ // currentSuggestion has key="" for nullselection
+ if (currentSuggestion != null
+ && !currentSuggestion.key.equals("")) {
+ // An item (not null) selected
+ String text = currentSuggestion.getReplacementString();
+ setText(text);
+ selectedOptionKey = currentSuggestion.key;
+ } else {
+ // Null selected
+ setText("");
+ selectedOptionKey = null;
+ }
+ }
+ suggestionPopup.hide();
+ }
+
+ private static final String SUBPART_PREFIX = "item";
+
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(
+ String subPart) {
+ int index = Integer.parseInt(subPart.substring(SUBPART_PREFIX
+ .length()));
+
+ MenuItem item = getItems().get(index);
+
+ return item.getElement();
+ }
+
+ @Override
+ public String getSubPartName(
+ com.google.gwt.user.client.Element subElement) {
+ if (!getElement().isOrHasChild(subElement)) {
+ return null;
+ }
+
+ Element menuItemRoot = subElement;
+ while (menuItemRoot != null
+ && !menuItemRoot.getTagName().equalsIgnoreCase("td")) {
+ 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;
+ }
+
+ @Override
+ public void onLoad(LoadEvent event) {
+ debug("VFS.SM: onLoad()");
+ // Handle icon onload events to ensure shadow is resized
+ // correctly
+ delayedImageLoadExecutioner.trigger();
+
+ }
+
+ /**
+ * @deprecated use {@link SuggestionPopup#selectFirstItem()} instead.
+ */
+ @Deprecated
+ public void selectFirstItem() {
+ debug("VFS.SM: selectFirstItem()");
+ MenuItem firstItem = getItems().get(0);
+ selectItem(firstItem);
+ }
+
+ /**
+ * @deprecated use {@link SuggestionPopup#selectLastItem()} instead.
+ */
+ @Deprecated
+ public void selectLastItem() {
+ debug("VFS.SM: selectLastItem()");
+ List<MenuItem> items = getItems();
+ MenuItem lastItem = items.get(items.size() - 1);
+ selectItem(lastItem);
+ }
+
+ /*
+ * Gets the height of one menu item.
+ */
+ int getItemOffsetHeight() {
+ List<MenuItem> items = getItems();
+ return items != null && items.size() > 0 ? items.get(0)
+ .getOffsetHeight() : 0;
+ }
+
+ /*
+ * Gets the width of one menu item.
+ */
+ int getItemOffsetWidth() {
+ List<MenuItem> items = getItems();
+ return items != null && items.size() > 0 ? items.get(0)
+ .getOffsetWidth() : 0;
+ }
+
+ /**
+ * Returns true if the scroll is active on the menu element or if the
+ * menu currently displays the last page with less items then the
+ * maximum visibility (in which case the scroll is not active, but the
+ * scroll is active for any other page in general).
+ *
+ * @since 7.2.6
+ */
+ @Override
+ public boolean isScrollActive() {
+ String height = getElement().getStyle().getHeight();
+ String preferredHeight = getPreferredHeight(pageLength);
+
+ return !(height == null || height.length() == 0 || height
+ .equals(preferredHeight));
+ }
+
+ }
+
+ /**
+ * TextBox variant used as input element for filter selects, which prevents
+ * selecting text when disabled.
+ *
+ * @since 7.1.5
+ */
+ public class FilterSelectTextBox extends TextBox {
+
++ /**
++ * Creates a new filter select text box.
++ *
++ * @since 7.6.4
++ */
++ public FilterSelectTextBox() {
++ /*-
++ * Stop the browser from showing its own suggestion popup.
++ *
++ * Using an invalid value instead of "off" as suggested by
++ * https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
++ *
++ * Leaving the non-standard Safari options autocapitalize and
++ * autocorrect untouched since those do not interfere in the same
++ * way, and they might be useful in a combo box where new items are
++ * allowed.
++ */
++ getElement().setAttribute("autocomplete", "nope");
++ }
++
+ /**
+ * Overridden to avoid selecting text when text input is disabled
+ */
+ @Override
+ public void setSelectionRange(int pos, int length) {
+ if (textInputEnabled) {
+ /*
+ * set selection range with a backwards direction: anchor at the
+ * back, focus at the front. This means that items that are too
+ * long to display will display from the start and not the end
+ * even on Firefox.
+ *
+ * We need the JSNI function to set selection range so that we
+ * can use the optional direction attribute to set the anchor to
+ * the end and the focus to the start. This makes Firefox work
+ * the same way as other browsers (#13477)
+ */
+ WidgetUtil.setSelectionRange(getElement(), pos, length,
+ "backward");
+
+ } else {
+ /*
+ * Setting the selectionrange for an uneditable textbox leads to
+ * unwanted behaviour when the width of the textbox is narrower
+ * than the width of the entry: the end of the entry is shown
+ * instead of the beginning. (see #13477)
+ *
+ * To avoid this, we set the caret to the beginning of the line.
+ */
+
+ super.setSelectionRange(0, 0);
+ }
+ }
+
+ }
+
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_OFF = FilteringMode.OFF;
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_STARTSWITH = FilteringMode.STARTSWITH;
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_CONTAINS = FilteringMode.CONTAINS;
+
+ public static final String CLASSNAME = "v-filterselect";
+ private static final String STYLE_NO_INPUT = "no-input";
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int pageLength = 10;
+
+ private boolean enableDebug = false;
+
+ private final FlowPanel panel = new FlowPanel();
+
+ /**
+ * The text box where the filter is written
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public final TextBox tb;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final SuggestionPopup suggestionPopup;
+
+ /**
+ * Used when measuring the width of the popup
+ */
+ private final HTML popupOpener = new HTML("") {
+
+ /*
+ * (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);
+
+ /*
+ * Prevent the keyboard focus from leaving the textfield by
+ * preventing the default behaviour of the browser. Fixes #4285.
+ */
+ handleMouseDownEvent(event);
+ }
+ };
+
+ private class IconWidget extends Widget {
+ IconWidget(Icon icon) {
+ setElement(icon.getElement());
+ addDomHandler(VFilterSelect.this, ClickEvent.getType());
+ }
+ }
+
+ private IconWidget selectedItemIcon;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public ApplicationConnection client;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String paintableId;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int currentPage;
+
+ /**
+ * A collection of available suggestions (options) as received from the
+ * server.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public final List<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>();
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean immediate;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String selectedOptionKey;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean waitingForFilteringResponse = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean updateSelectionWhenReponseIsReceived = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean initDone = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String lastFilter = "";
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public enum Select {
+ NONE, FIRST, LAST
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public Select selectPopupItemWhenResponseIsReceived = Select.NONE;
+
+ /**
+ * The current suggestion selected from the dropdown. This is one of the
+ * values in currentSuggestions except when filtering, in this case
+ * currentSuggestion might not be in currentSuggestions.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public FilterSelectSuggestion currentSuggestion;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean allowNewItem;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int totalMatches;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean nullSelectionAllowed;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean nullSelectItem;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean enabled;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean readonly;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public FilteringMode filteringmode = FilteringMode.OFF;
+
+ // shown in unfocused empty field, disappears on focus (e.g "Search here")
+ private static final String CLASSNAME_PROMPT = "prompt";
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String inputPrompt = "";
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean prompting = false;
+
+ /**
+ * Set true when popupopened has been clicked. Cleared on each UIDL-update.
+ * This handles the special case where are not filtering yet and the
+ * selected value has changed on the server-side. See #2119
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public boolean popupOpenerClicked;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int suggestionPopupMinWidth = 0;
+
+ private int popupWidth = -1;
+ /**
+ * Stores the last new item string to avoid double submissions. Cleared on
+ * uidl updates.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public String lastNewItemString;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean focused = false;
+
+ /**
+ * If set to false, the component should not allow entering text to the
+ * field even for filtering.
+ */
+ private boolean textInputEnabled = true;
+
+ /**
+ * Default constructor.
+ */
+ public VFilterSelect() {
+ tb = createTextBox();
+ suggestionPopup = createSuggestionPopup();
+
+ popupOpener.sinkEvents(Event.ONMOUSEDOWN);
+ Roles.getButtonRole()
+ .setAriaHiddenState(popupOpener.getElement(), true);
+ Roles.getButtonRole().set(popupOpener.getElement());
+
+ panel.add(tb);
+ panel.add(popupOpener);
+ initWidget(panel);
+ Roles.getComboboxRole().set(panel.getElement());
+
+ tb.addKeyDownHandler(this);
+ tb.addKeyUpHandler(this);
+
+ tb.addFocusHandler(this);
+ tb.addBlurHandler(this);
+ tb.addClickHandler(this);
+
+ popupOpener.addClickHandler(this);
+
+ setStyleName(CLASSNAME);
+
+ sinkEvents(Event.ONPASTE);
+ }
+
+ private static double getMarginBorderPaddingWidth(Element element) {
+ final ComputedStyle s = new ComputedStyle(element);
+ return s.getMarginWidth() + s.getBorderWidth() + s.getPaddingWidth();
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Composite#onBrowserEvent(com.google.gwt
+ * .user.client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+
+ if (event.getTypeInt() == Event.ONPASTE) {
+ if (textInputEnabled) {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ filterOptions(currentPage);
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * This method will create the TextBox used by the VFilterSelect instance.
+ * It is invoked during the Constructor and should only be overridden if a
+ * custom TextBox shall be used. The overriding method cannot use any
+ * instance variables.
+ *
+ * @since 7.1.5
+ * @return TextBox instance used by this VFilterSelect
+ */
+ protected TextBox createTextBox() {
+ return new FilterSelectTextBox();
+ }
+
+ /**
+ * This method will create the SuggestionPopup used by the VFilterSelect
+ * instance. It is invoked during the Constructor and should only be
+ * overridden if a custom SuggestionPopup shall be used. The overriding
+ * method cannot use any instance variables.
+ *
+ * @since 7.1.5
+ * @return SuggestionPopup instance used by this VFilterSelect
+ */
+ protected SuggestionPopup createSuggestionPopup() {
+ return new SuggestionPopup();
+ }
+
+ @Override
+ public void setStyleName(String style) {
+ super.setStyleName(style);
+ updateStyleNames();
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+ updateStyleNames();
+ }
+
+ protected void updateStyleNames() {
+ tb.setStyleName(getStylePrimaryName() + "-input");
+ popupOpener.setStyleName(getStylePrimaryName() + "-button");
+ suggestionPopup.setStyleName(getStylePrimaryName() + "-suggestpopup");
+ }
+
+ /**
+ * Does the Select have more pages?
+ *
+ * @return true if a next page exists, else false if the current page is the
+ * last page
+ */
+ public boolean hasNextPage() {
+ if (pageLength > 0 && totalMatches > (currentPage + 1) * pageLength) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Filters the options at a certain page. Uses the text box input as a
+ * filter
+ *
+ * @param page
+ * The page which items are to be filtered
+ */
+ public void filterOptions(int page) {
+ filterOptions(page, tb.getText());
+ }
+
+ /**
+ * Filters the options at certain page using the given filter
+ *
+ * @param page
+ * The page to filter
+ * @param filter
+ * The filter to apply to the components
+ */
+ public void filterOptions(int page, String filter) {
+ filterOptions(page, filter, true);
+ }
+
+ /**
+ * Filters the options at certain page using the given filter
+ *
+ * @param page
+ * The page to filter
+ * @param filter
+ * The filter to apply to the options
+ * @param immediate
+ * Whether to send the options request immediately
+ */
+ private void filterOptions(int page, String filter, boolean immediate) {
+ debug("VFS: filterOptions(" + page + ", " + filter + ", " + immediate
+ + ")");
+
+ if (filter.equals(lastFilter) && currentPage == page) {
+ if (!suggestionPopup.isAttached()) {
+ suggestionPopup.showSuggestions(currentSuggestions,
+ currentPage, totalMatches);
+ }
+ return;
+ }
+ if (!filter.equals(lastFilter)) {
+ // when filtering, let the server decide the page unless we've
+ // set the filter to empty and explicitly said that we want to see
+ // the results starting from page 0.
+ if ("".equals(filter) && page != 0) {
+ // let server decide
+ page = -1;
+ } else {
+ page = 0;
+ }
+ }
+
+ waitingForFilteringResponse = true;
+ client.updateVariable(paintableId, "filter", filter, false);
+ client.updateVariable(paintableId, "page", page, immediate);
+ afterUpdateClientVariables();
+
+ lastFilter = filter;
+ currentPage = page;
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void updateReadOnly() {
+ debug("VFS: updateReadOnly()");
+ tb.setReadOnly(readonly || !textInputEnabled);
+ }
+
+ public void setTextInputEnabled(boolean textInputEnabled) {
+ debug("VFS: setTextInputEnabled()");
+ // Always update styles as they might have been overwritten
+ if (textInputEnabled) {
+ removeStyleDependentName(STYLE_NO_INPUT);
+ Roles.getTextboxRole().removeAriaReadonlyProperty(tb.getElement());
+ } else {
+ addStyleDependentName(STYLE_NO_INPUT);
+ Roles.getTextboxRole().setAriaReadonlyProperty(tb.getElement(),
+ true);
+ }
+
+ if (this.textInputEnabled == textInputEnabled) {
+ return;
+ }
+
+ this.textInputEnabled = textInputEnabled;
+ updateReadOnly();
+ }
+
+ /**
+ * Sets the text in the text box.
+ *
+ * @param text
+ * the text to set in the text box
+ */
+ public void setTextboxText(final String text) {
+ if (enableDebug) {
+ debug("VFS: setTextboxText(" + text + ")");
+ }
+ setText(text);
+ }
+
+ private void setText(final String text) {
+ /**
+ * To leave caret in the beginning of the line. SetSelectionRange
+ * wouldn't work on IE (see #13477)
+ */
+ Direction previousDirection = tb.getDirection();
+ tb.setDirection(Direction.RTL);
+ tb.setText(text);
+ tb.setDirection(previousDirection);
+ }
+
+ /**
+ * Turns prompting on. When prompting is turned on a command prompt is shown
+ * in the text box if nothing has been entered.
+ */
+ public void setPromptingOn() {
+ debug("VFS: setPromptingOn()");
+ if (!prompting) {
+ prompting = true;
+ addStyleDependentName(CLASSNAME_PROMPT);
+ }
+ setTextboxText(inputPrompt);
+ }
+
+ /**
+ * Turns prompting off. When prompting is turned on a command prompt is
+ * shown in the text box if nothing has been entered.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param text
+ * The text the text box should contain.
+ */
+ public void setPromptingOff(String text) {
+ debug("VFS: setPromptingOff()");
+ setTextboxText(text);
+ if (prompting) {
+ prompting = false;
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ }
+
+ /**
+ * Triggered when a suggestion is selected
+ *
+ * @param suggestion
+ * The suggestion that just got selected.
+ */
+ public void onSuggestionSelected(FilterSelectSuggestion suggestion) {
+ if (enableDebug) {
+ debug("VFS: onSuggestionSelected(" + suggestion.caption + ": "
+ + suggestion.key + ")");
+ }
+ updateSelectionWhenReponseIsReceived = false;
+
+ currentSuggestion = suggestion;
+ String newKey;
+ if (suggestion.key.equals("")) {
+ // "nullselection"
+ newKey = "";
+ } else {
+ // normal selection
+ newKey = suggestion.getOptionKey();
+ }
+
+ String text = suggestion.getReplacementString();
+ if ("".equals(newKey) && !focused) {
+ setPromptingOn();
+ } else {
+ setPromptingOff(text);
+ }
+ setSelectedItemIcon(suggestion.getIconUri());
+
+ if (!(newKey.equals(selectedOptionKey) || ("".equals(newKey) && selectedOptionKey == null))) {
+ selectedOptionKey = newKey;
+ client.updateVariable(paintableId, "selected",
+ new String[] { selectedOptionKey }, immediate);
+ afterUpdateClientVariables();
+
+ // currentPage = -1; // forget the page
+ }
+ suggestionPopup.hide();
+ }
+
+ /**
+ * Sets the icon URI of the selected item. The icon is shown on the left
+ * side of the item caption text. Set the URI to null to remove the icon.
+ *
+ * @param iconUri
+ * The URI of the icon
+ */
+ public void setSelectedItemIcon(String iconUri) {
+
+ if (iconUri == null || iconUri.length() == 0) {
+ if (selectedItemIcon != null) {
+ panel.remove(selectedItemIcon);
+ selectedItemIcon = null;
+ afterSelectedItemIconChange();
+ }
+ } else {
+ if (selectedItemIcon != null) {
+ panel.remove(selectedItemIcon);
+ }
+ selectedItemIcon = new IconWidget(client.getIcon(iconUri));
+ // Older IE versions don't scale icon correctly if DOM
+ // contains height and width attributes.
+ selectedItemIcon.getElement().removeAttribute("height");
+ selectedItemIcon.getElement().removeAttribute("width");
+ selectedItemIcon.addDomHandler(new LoadHandler() {
+ @Override
+ public void onLoad(LoadEvent event) {
+ afterSelectedItemIconChange();
+ }
+ }, LoadEvent.getType());
+ panel.insert(selectedItemIcon, 0);
+ afterSelectedItemIconChange();
+ }
+ }
+
+ private void afterSelectedItemIconChange() {
+ if (BrowserInfo.get().isWebkit() || BrowserInfo.get().isIE8()) {
+ // Some browsers need a nudge to reposition the text field
+ forceReflow();
+ }
+ updateRootWidth();
+ if (selectedItemIcon != null) {
+ updateSelectedIconPosition();
+ }
+ }
+
+ private void forceReflow() {
+ WidgetUtil.setStyleTemporarily(tb.getElement(), "zoom", "1");
+ }
+
+ /**
+ * Positions the icon vertically in the middle. Should be called after the
+ * icon has loaded
+ */
+ private void updateSelectedIconPosition() {
+ // Position icon vertically to middle
+ int availableHeight = 0;
+ availableHeight = getOffsetHeight();
+
+ int iconHeight = WidgetUtil.getRequiredHeight(selectedItemIcon);
+ int marginTop = (availableHeight - iconHeight) / 2;
+ selectedItemIcon.getElement().getStyle()
+ .setMarginTop(marginTop, Unit.PX);
+ }
+
+ private static Set<Integer> navigationKeyCodes = new HashSet<Integer>();
+ static {
+ navigationKeyCodes.add(KeyCodes.KEY_DOWN);
+ navigationKeyCodes.add(KeyCodes.KEY_UP);
+ navigationKeyCodes.add(KeyCodes.KEY_PAGEDOWN);
+ navigationKeyCodes.add(KeyCodes.KEY_PAGEUP);
+ navigationKeyCodes.add(KeyCodes.KEY_ENTER);
+ }
+
+ /*
+ * (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 (enabled && !readonly) {
+ int keyCode = event.getNativeKeyCode();
+
+ if (enableDebug) {
+ debug("VFS: key down: " + keyCode);
+ }
+ if (waitingForFilteringResponse
+ && navigationKeyCodes.contains(keyCode)) {
+ /*
+ * Keyboard navigation events should not be handled while we are
+ * waiting for a response. This avoids flickering, disappearing
+ * items, wrongly interpreted responses and more.
+ */
+ if (enableDebug) {
+ debug("Ignoring "
+ + keyCode
+ + " because we are waiting for a filtering response");
+ }
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ return;
+ }
+
+ if (suggestionPopup.isAttached()) {
+ if (enableDebug) {
+ debug("Keycode " + keyCode + " target is popup");
+ }
+ popupKeyDown(event);
+ } else {
+ if (enableDebug) {
+ debug("Keycode " + keyCode + " target is text field");
+ }
+ inputFieldKeyDown(event);
+ }
+ }
+ }
+
+ private void debug(String string) {
+ if (enableDebug) {
+ VConsole.error(string);
+ }
+ }
+
+ /**
+ * Triggered when a key is pressed in the text box
+ *
+ * @param event
+ * The KeyDownEvent
+ */
+ private void inputFieldKeyDown(KeyDownEvent event) {
+ if (enableDebug) {
+ debug("VFS: inputFieldKeyDown(" + event.getNativeKeyCode() + ")");
+ }
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_PAGEDOWN:
+ case KeyCodes.KEY_PAGEUP:
+ // open popup as from gadget
+ filterOptions(-1, "");
+ lastFilter = "";
+ tb.selectAll();
+ break;
+ case KeyCodes.KEY_ENTER:
+ /*
+ * This only handles the case when new items is allowed, a text is
+ * entered, the popup opener button is clicked to close the popup
+ * and enter is then pressed (see #7560).
+ */
+ if (!allowNewItem) {
+ return;
+ }
+
+ if (currentSuggestion != null
+ && tb.getText().equals(
+ currentSuggestion.getReplacementString())) {
+ // Retain behavior from #6686 by returning without stopping
+ // propagation if there's nothing to do
+ return;
+ }
+ suggestionPopup.menu.doSelectedItemAction();
+
+ event.stopPropagation();
+ break;
+ }
+
+ }
+
+ /**
+ * Triggered when a key was pressed in the suggestion popup.
+ *
+ * @param event
+ * The KeyDownEvent of the key
+ */
+ private void popupKeyDown(KeyDownEvent event) {
+ if (enableDebug) {
+ debug("VFS: popupKeyDown(" + event.getNativeKeyCode() + ")");
+ }
+ // Propagation of handled events is stopped so other handlers such as
+ // shortcut key handlers do not also handle the same events.
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ suggestionPopup.selectNextItem();
+
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_UP:
+ suggestionPopup.selectPrevItem();
+
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_PAGEDOWN:
+ selectNextPage();
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_PAGEUP:
+ selectPrevPage();
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_ESCAPE:
+ reset();
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_TAB:
+ case KeyCodes.KEY_ENTER:
+
+ if (!allowNewItem) {
+ int selected = suggestionPopup.menu.getSelectedIndex();
+ if (selected != -1) {
+ onSuggestionSelected(currentSuggestions.get(selected));
+ } else {
+ // The way VFilterSelect is done, it handles enter and tab
+ // in exactly the same way so we close the popup in both
+ // cases even though we could leave it open when pressing
+ // enter
+ suggestionPopup.hide();
+ }
+ } else {
+ // Handle addition of new items.
+ suggestionPopup.menu.doSelectedItemAction();
+ }
+
+ event.stopPropagation();
+ break;
+ }
+
+ }
+
+ /*
+ * Show the prev page.
+ */
+ private void selectPrevPage() {
+ if (currentPage > 0) {
+ filterOptions(currentPage - 1, lastFilter);
+ selectPopupItemWhenResponseIsReceived = Select.LAST;
+ }
+ }
+
+ /*
+ * Show the next page.
+ */
+ private void selectNextPage() {
+ if (hasNextPage()) {
+ filterOptions(currentPage + 1, lastFilter);
+ selectPopupItemWhenResponseIsReceived = Select.FIRST;
+ }
+ }
+
+ /**
+ * Triggered when a key was depressed
+ *
+ * @param event
+ * The KeyUpEvent of the key depressed
+ */
+
+ @Override
+ public void onKeyUp(KeyUpEvent event) {
+ if (enableDebug) {
+ debug("VFS: onKeyUp(" + event.getNativeKeyCode() + ")");
+ }
+ if (enabled && !readonly) {
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_ENTER:
+ case KeyCodes.KEY_TAB:
+ case KeyCodes.KEY_SHIFT:
+ case KeyCodes.KEY_CTRL:
+ case KeyCodes.KEY_ALT:
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_PAGEDOWN:
+ case KeyCodes.KEY_PAGEUP:
+ case KeyCodes.KEY_ESCAPE:
+ // NOP
+ break;
+ default:
+ if (textInputEnabled) {
+ // when filtering, we always want to see the results on the
+ // first page first.
+ filterOptions(0);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Resets the Select to its initial state
+ */
+ private void reset() {
+ debug("VFS: reset()");
+ if (currentSuggestion != null) {
+ String text = currentSuggestion.getReplacementString();
+ setPromptingOff(text);
+ setSelectedItemIcon(currentSuggestion.getIconUri());
+
+ selectedOptionKey = currentSuggestion.key;
+
+ } else {
+ if (focused || readonly || !enabled) {
+ setPromptingOff("");
+ } else {
+ setPromptingOn();
+ }
+ setSelectedItemIcon(null);
+
+ selectedOptionKey = null;
+ }
+
+ lastFilter = "";
+ suggestionPopup.hide();
+ }
+
+ /**
+ * Listener for popupopener
+ */
+
+ @Override
+ public void onClick(ClickEvent event) {
+ debug("VFS: onClick()");
+ if (textInputEnabled
+ && event.getNativeEvent().getEventTarget().cast() == tb
+ .getElement()) {
+ // Don't process clicks on the text field if text input is enabled
+ return;
+ }
+ if (enabled && !readonly) {
+ // ask suggestionPopup if it was just closed, we are using GWT
+ // Popup's auto close feature
+ if (!suggestionPopup.isJustClosed()) {
+ // If a focus event is not going to be sent, send the options
+ // request immediately; otherwise queue in the same burst as the
+ // focus event. Fixes #8321.
+ boolean immediate = focused
+ || !client.hasEventListeners(this, EventId.FOCUS);
+ filterOptions(-1, "", immediate);
+ popupOpenerClicked = true;
+ lastFilter = "";
+ }
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ focus();
+ tb.selectAll();
+ }
+ }
+
+ /**
+ * Update minimum width for FilterSelect textarea based on input prompt and
+ * suggestions.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public void updateSuggestionPopupMinWidth() {
+ // used only to calculate minimum width
+ String captions = WidgetUtil.escapeHTML(inputPrompt);
+
+ for (FilterSelectSuggestion suggestion : currentSuggestions) {
+ // Collect captions so we can calculate minimum width for
+ // textarea
+ if (captions.length() > 0) {
+ captions += "|";
+ }
+ captions += WidgetUtil
+ .escapeHTML(suggestion.getReplacementString());
+ }
+
+ // Calculate minimum textarea width
+ suggestionPopupMinWidth = minWidth(captions);
+ }
+
+ /**
+ * Calculate minimum width for FilterSelect textarea.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public native int minWidth(String captions)
+ /*-{
+ if(!captions || captions.length <= 0)
+ return 0;
+ captions = captions.split("|");
+ var d = $wnd.document.createElement("div");
+ var html = "";
+ for(var i=0; i < captions.length; i++) {
+ html += "<div>" + captions[i] + "</div>";
+ // TODO apply same CSS classname as in suggestionmenu
+ }
+ d.style.position = "absolute";
+ d.style.top = "0";
+ d.style.left = "0";
+ d.style.visibility = "hidden";
+ d.innerHTML = html;
+ $wnd.document.body.appendChild(d);
+ var w = d.offsetWidth;
+ $wnd.document.body.removeChild(d);
+ return w;
+ }-*/;
+
+ /**
+ * A flag which prevents a focus event from taking place
+ */
+ boolean iePreventNextFocus = false;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ debug("VFS: onFocus()");
+
+ /*
+ * When we disable a blur event in ie we need to refocus the textfield.
+ * This will cause a focus event we do not want to process, so in that
+ * case we just ignore it.
+ */
+ if (BrowserInfo.get().isIE() && iePreventNextFocus) {
+ iePreventNextFocus = false;
+ return;
+ }
+
+ focused = true;
+ if (prompting && !readonly) {
+ setPromptingOff("");
+ }
+ addStyleDependentName("focus");
+
+ if (client.hasEventListeners(this, EventId.FOCUS)) {
+ client.updateVariable(paintableId, EventId.FOCUS, "", true);
+ afterUpdateClientVariables();
+ }
+
+ ComponentConnector connector = ConnectorMap.get(client).getConnector(
+ this);
+ client.getVTooltip().showAssistive(
+ connector.getTooltipInfo(getElement()));
+ }
+
+ /**
+ * A flag which cancels the blur event and sets the focus back to the
+ * textfield if the Browser is IE
+ */
+ boolean preventNextBlurEventInIE = 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) {
+ debug("VFS: onBlur()");
+
+ if (BrowserInfo.get().isIE() && preventNextBlurEventInIE) {
+ /*
+ * Clicking in the suggestion popup or on the popup button in IE
+ * causes a blur event to be sent for the field. In other browsers
+ * this is prevented by canceling/preventing default behavior for
+ * the focus event, in IE we handle it here by refocusing the text
+ * field and ignoring the resulting focus event for the textfield
+ * (in onFocus).
+ */
+ preventNextBlurEventInIE = false;
+
+ Element focusedElement = WidgetUtil.getFocusedElement();
+ if (getElement().isOrHasChild(focusedElement)
+ || suggestionPopup.getElement()
+ .isOrHasChild(focusedElement)) {
+
+ // IF the suggestion popup or another part of the VFilterSelect
+ // was focused, move the focus back to the textfield and prevent
+ // the triggered focus event (in onFocus).
+ iePreventNextFocus = true;
+ tb.setFocus(true);
+ return;
+ }
+ }
+
+ focused = false;
+ if (!readonly) {
+ if (selectedOptionKey == null) {
+ setPromptingOn();
+ } else if (currentSuggestion != null) {
+ setPromptingOff(currentSuggestion.caption);
+ }
+ }
+ removeStyleDependentName("focus");
+
+ if (client.hasEventListeners(this, EventId.BLUR)) {
+ client.updateVariable(paintableId, EventId.BLUR, "", true);
+ afterUpdateClientVariables();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.Focusable#focus()
+ */
+
+ @Override
+ public void focus() {
+ debug("VFS: focus()");
+ focused = true;
+ if (prompting && !readonly) {
+ setPromptingOff("");
+ }
+ tb.setFocus(true);
+ }
+
+ /**
+ * Calculates the width of the select if the select has undefined width.
+ * Should be called when the width changes or when the icon changes.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public void updateRootWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+
+ if (paintable.isUndefinedWidth()) {
+
+ /*
+ * When the select has a undefined with we need to check that we are
+ * only setting the text box width relative to the first page width
+ * of the items. If this is not done the text box width will change
+ * when the popup is used to view longer items than the text box is
+ * wide.
+ */
+ int w = WidgetUtil.getRequiredWidth(this);
+
+ if ((!initDone || currentPage + 1 < 0)
+ && suggestionPopupMinWidth > w) {
+ /*
+ * We want to compensate for the paddings just to preserve the
+ * exact size as in Vaadin 6.x, but we get here before
+ * MeasuredSize has been initialized.
+ * Util.measureHorizontalPaddingAndBorder does not work with
+ * border-box, so we must do this the hard way.
+ */
+ Style style = getElement().getStyle();
+ String originalPadding = style.getPadding();
+ String originalBorder = style.getBorderWidth();
+ style.setPaddingLeft(0, Unit.PX);
+ style.setBorderWidth(0, Unit.PX);
+ style.setProperty("padding", originalPadding);
+ style.setProperty("borderWidth", originalBorder);
+
+ // Use util.getRequiredWidth instead of getOffsetWidth here
+
+ int iconWidth = selectedItemIcon == null ? 0 : WidgetUtil
+ .getRequiredWidth(selectedItemIcon);
+ int buttonWidth = popupOpener == null ? 0 : WidgetUtil
+ .getRequiredWidth(popupOpener);
+
+ /*
+ * Instead of setting the width of the wrapper, set the width of
+ * the combobox. Subtract the width of the icon and the
+ * popupopener
+ */
+
+ tb.setWidth((suggestionPopupMinWidth - iconWidth - buttonWidth)
+ + "px");
+
+ }
+
+ /*
+ * Lock the textbox width to its current value if it's not already
+ * locked
+ */
+ if (!tb.getElement().getStyle().getWidth().endsWith("px")) {
+ int iconWidth = selectedItemIcon == null ? 0 : selectedItemIcon
+ .getOffsetWidth();
+ tb.setWidth((tb.getOffsetWidth() - iconWidth) + "px");
+ }
+ }
+ }
+
+ /**
+ * Get the width of the select in pixels where the text area and icon has
+ * been included.
+ *
+ * @return The width in pixels
+ */
+ private int getMainWidth() {
+ return getOffsetWidth();
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (width.length() != 0) {
+ tb.setWidth("100%");
+ }
+ }
+
+ /**
+ * Handles special behavior of the mouse down event
+ *
+ * @param event
+ */
+ private void handleMouseDownEvent(Event event) {
+ /*
+ * Prevent the keyboard focus from leaving the textfield by preventing
+ * the default behaviour of the browser. Fixes #4285.
+ */
+ if (event.getTypeInt() == Event.ONMOUSEDOWN) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ /*
+ * In IE the above wont work, the blur event will still trigger. So,
+ * we set a flag here to prevent the next blur event from happening.
+ * This is not needed if do not already have focus, in that case
+ * there will not be any blur event and we should not cancel the
+ * next blur.
+ */
+ if (BrowserInfo.get().isIE() && focused) {
+ preventNextBlurEventInIE = true;
+ debug("VFS: Going to prevent next blur event on IE");
+ }
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ suggestionPopup.hide();
+ }
+
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
+ String[] parts = subPart.split("/");
+ if ("textbox".equals(parts[0])) {
+ return tb.getElement();
+ } else if ("button".equals(parts[0])) {
+ return popupOpener.getElement();
+ } else if ("popup".equals(parts[0]) && suggestionPopup.isAttached()) {
+ if (parts.length == 2) {
+ return suggestionPopup.menu.getSubPartElement(parts[1]);
+ }
+ return suggestionPopup.getElement();
+ }
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(com.google.gwt.user.client.Element subElement) {
+ if (tb.getElement().isOrHasChild(subElement)) {
+ return "textbox";
+ } else if (popupOpener.getElement().isOrHasChild(subElement)) {
+ return "button";
+ } else if (suggestionPopup.getElement().isOrHasChild(subElement)) {
+ return "popup";
+ }
+ return null;
+ }
+
+ @Override
+ public void setAriaRequired(boolean required) {
+ AriaHelper.handleInputRequired(tb, required);
+ }
+
+ @Override
+ public void setAriaInvalid(boolean invalid) {
+ AriaHelper.handleInputInvalid(tb, invalid);
+ }
+
+ @Override
+ public void bindAriaCaption(
+ com.google.gwt.user.client.Element captionElement) {
+ AriaHelper.bindCaption(tb, captionElement);
+ }
+
+ /*
+ * Anything that should be set after the client updates the server.
+ */
+ private void afterUpdateClientVariables() {
+ // We need this here to be consistent with the all the calls.
+ // Then set your specific selection type only after
+ // client.updateVariable() method call.
+ selectPopupItemWhenResponseIsReceived = Select.NONE;
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return waitingForFilteringResponse
+ || suggestionPopup.lazyPageScroller.isRunning();
+ }
+
+}
--- /dev/null
- implements Paintable {
+/*
+ * Copyright 2000-2014 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.client.ui.draganddropwrapper;
+
+import java.util.HashMap;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorMap;
+import com.vaadin.client.Paintable;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.VConsole;
+import com.vaadin.client.ui.VDragAndDropWrapper;
+import com.vaadin.client.ui.customcomponent.CustomComponentConnector;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.draganddropwrapper.DragAndDropWrapperConstants;
++import com.vaadin.shared.ui.draganddropwrapper.DragAndDropWrapperServerRpc;
+import com.vaadin.ui.DragAndDropWrapper;
+
+@Connect(DragAndDropWrapper.class)
+public class DragAndDropWrapperConnector extends CustomComponentConnector
++ implements Paintable, VDragAndDropWrapper.UploadHandler {
++
++ @Override
++ protected void init() {
++ super.init();
++ getWidget().uploadHandler = this;
++ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().client = client;
+ if (isRealUpdate(uidl) && !uidl.hasAttribute("hidden")) {
+ UIDL acceptCrit = uidl.getChildByTagName("-ac");
+ if (acceptCrit == null) {
+ getWidget().dropHandler = null;
+ } else {
+ if (getWidget().dropHandler == null) {
+ getWidget().dropHandler = getWidget().new CustomDropHandler();
+ }
+ getWidget().dropHandler.updateAcceptRules(acceptCrit);
+ }
+
+ Set<String> variableNames = uidl.getVariableNames();
+ for (String fileId : variableNames) {
+ if (fileId.startsWith("rec-")) {
+ String receiverUrl = uidl.getStringVariable(fileId);
+ fileId = fileId.substring(4);
+ if (getWidget().fileIdToReceiver == null) {
+ getWidget().fileIdToReceiver = new HashMap<String, String>();
+ }
+ if ("".equals(receiverUrl)) {
+ Integer id = Integer.parseInt(fileId);
+ int indexOf = getWidget().fileIds.indexOf(id);
+ if (indexOf != -1) {
+ getWidget().files.remove(indexOf);
+ getWidget().fileIds.remove(indexOf);
+ }
+ } else {
+ if (getWidget().fileIdToReceiver.containsKey(fileId)
+ && receiverUrl != null
+ && !receiverUrl
+ .equals(getWidget().fileIdToReceiver
+ .get(fileId))) {
+ VConsole.error("Overwriting file receiver mapping for fileId "
+ + fileId
+ + " . Old receiver URL: "
+ + getWidget().fileIdToReceiver.get(fileId)
+ + " New receiver URL: " + receiverUrl);
+ }
+ getWidget().fileIdToReceiver.put(fileId, receiverUrl);
+ }
+ }
+ }
+ getWidget().startNextUpload();
+
+ getWidget().dragStartMode = uidl
+ .getIntAttribute(DragAndDropWrapperConstants.DRAG_START_MODE);
+
+ String dragImageComponentConnectorId = uidl
+ .getStringAttribute(DragAndDropWrapperConstants.DRAG_START_COMPONENT_ATTRIBUTE);
+
+ ComponentConnector connector = null;
+ if (dragImageComponentConnectorId != null) {
+ connector = (ComponentConnector) ConnectorMap.get(client)
+ .getConnector(dragImageComponentConnectorId);
+
+ if (connector == null) {
+ getLogger().log(
+ Level.WARNING,
+ "DragAndDropWrapper drag image component"
+ + " connector now found. Make sure the"
+ + " component is attached.");
+ } else {
+ getWidget().setDragAndDropWidget(connector.getWidget());
+ }
+ }
+ getWidget().initDragStartMode();
+ getWidget().html5DataFlavors = uidl
+ .getMapAttribute(DragAndDropWrapperConstants.HTML5_DATA_FLAVORS);
+
+ // Used to prevent wrapper from stealing tooltips when not defined
+ getWidget().hasTooltip = getState().description != null;
+ }
+ }
+
+ @Override
+ public VDragAndDropWrapper getWidget() {
+ return (VDragAndDropWrapper) super.getWidget();
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(DragAndDropWrapperConnector.class.getName());
+ }
++
++ @Override
++ public void uploadDone() {
++ // #19616 RPC to poll the server for changes
++ getRpcProxy(DragAndDropWrapperServerRpc.class).poll();
++ }
++
+}
--- /dev/null
- getWidget().enabled = isEnabled();
-
- if (BrowserInfo.get().isIE8() && !getWidget().enabled) {
- /*
- * The disabled shim will not cover the table body if it is relative
- * in IE8. See #7324
- */
- getWidget().scrollBodyPanel.getElement().getStyle()
- .setPosition(Position.STATIC);
- } else if (BrowserInfo.get().isIE8()) {
- getWidget().scrollBodyPanel.getElement().getStyle()
- .setPosition(Position.RELATIVE);
- }
-
+/*
+ * Copyright 2000-2014 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.client.ui.table;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler;
+import com.vaadin.client.DirectionalManagedLayout;
+import com.vaadin.client.HasChildMeasurementHintConnector;
+import com.vaadin.client.HasComponentsConnector;
+import com.vaadin.client.Paintable;
+import com.vaadin.client.ServerConnector;
+import com.vaadin.client.TooltipInfo;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.AbstractFieldConnector;
+import com.vaadin.client.ui.PostLayoutListener;
+import com.vaadin.client.ui.VScrollTable;
+import com.vaadin.client.ui.VScrollTable.ContextMenuDetails;
+import com.vaadin.client.ui.VScrollTable.FooterCell;
+import com.vaadin.client.ui.VScrollTable.HeaderCell;
+import com.vaadin.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.table.TableConstants;
+import com.vaadin.shared.ui.table.TableConstants.Section;
+import com.vaadin.shared.ui.table.TableServerRpc;
+import com.vaadin.shared.ui.table.TableState;
+
+@Connect(com.vaadin.ui.Table.class)
+public class TableConnector extends AbstractFieldConnector implements
+ HasComponentsConnector, ConnectorHierarchyChangeHandler, Paintable,
+ DirectionalManagedLayout, PostLayoutListener,
+ HasChildMeasurementHintConnector {
+
+ private List<ComponentConnector> childComponents;
+
+ public TableConnector() {
+ addConnectorHierarchyChangeHandler(this);
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().init(getConnection());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.ui.AbstractComponentConnector#onUnregister()
+ */
+ @Override
+ public void onUnregister() {
+ super.onUnregister();
+ getWidget().onUnregister();
+ }
+
+ @Override
+ protected void sendContextClickEvent(MouseEventDetails details,
+ EventTarget eventTarget) {
+
+ if (!Element.is(eventTarget)) {
+ return;
+ }
+ Element e = Element.as(eventTarget);
+
+ Section section;
+ String colKey = null;
+ String rowKey = null;
+ if (getWidget().tFoot.getElement().isOrHasChild(e)) {
+ section = Section.FOOTER;
+ FooterCell w = WidgetUtil.findWidget(e, FooterCell.class);
+ colKey = w.getColKey();
+ } else if (getWidget().tHead.getElement().isOrHasChild(e)) {
+ section = Section.HEADER;
+ HeaderCell w = WidgetUtil.findWidget(e, HeaderCell.class);
+ colKey = w.getColKey();
+ } else {
+ section = Section.BODY;
+ if (getWidget().scrollBody.getElement().isOrHasChild(e)) {
+ VScrollTableRow w = getScrollTableRow(e);
+ /*
+ * if w is null because we've clicked on an empty area, we will
+ * let rowKey and colKey be null too, which will then lead to
+ * the server side returning a null object.
+ */
+ if (w != null) {
+ rowKey = w.getKey();
+ colKey = getWidget().tHead.getHeaderCell(
+ getElementIndex(e, w.getElement())).getColKey();
+ }
+ }
+ }
+
+ getRpcProxy(TableServerRpc.class).contextClick(rowKey, colKey, section,
+ details);
+
+ WidgetUtil.clearTextSelection();
+ }
+
+ protected VScrollTableRow getScrollTableRow(Element e) {
+ return WidgetUtil.findWidget(e, VScrollTableRow.class);
+ }
+
+ private int getElementIndex(Element e,
+ com.google.gwt.user.client.Element element) {
+ int i = 0;
+ Element current = element.getFirstChildElement();
+ while (!current.isOrHasChild(e)) {
+ current = current.getNextSiblingElement();
+ ++i;
+ }
+ return i;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.Paintable#updateFromUIDL(com.vaadin.client.UIDL,
+ * com.vaadin.client.ApplicationConnection)
+ */
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().rendering = true;
+
+ // If a row has an open context menu, it will be closed as the row is
+ // detached. Retain a reference here so we can restore the menu if
+ // required.
+ ContextMenuDetails contextMenuBeforeUpdate = getWidget().contextMenu;
+
+ if (uidl.hasAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST)) {
+ getWidget().serverCacheFirst = uidl
+ .getIntAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST);
+ getWidget().serverCacheLast = uidl
+ .getIntAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_LAST);
+ } else {
+ getWidget().serverCacheFirst = -1;
+ getWidget().serverCacheLast = -1;
+ }
+ /*
+ * We need to do this before updateComponent since updateComponent calls
+ * this.setHeight() which will calculate a new body height depending on
+ * the space available.
+ */
+ if (uidl.hasAttribute("colfooters")) {
+ getWidget().showColFooters = uidl.getBooleanAttribute("colfooters");
+ }
+
+ getWidget().tFoot.setVisible(getWidget().showColFooters);
+
+ if (!isRealUpdate(uidl)) {
+ getWidget().rendering = false;
+ return;
+ }
+
+ getWidget().paintableId = uidl.getStringAttribute("id");
+ getWidget().immediate = getState().immediate;
+
+ int previousTotalRows = getWidget().totalRows;
+ getWidget().updateTotalRows(uidl);
+ boolean totalRowsHaveChanged = (getWidget().totalRows != previousTotalRows);
+
+ getWidget().updateDragMode(uidl);
+
+ // Update child measure hint
+ int childMeasureHint = uidl.hasAttribute("measurehint") ? uidl
+ .getIntAttribute("measurehint") : 0;
+ getWidget().setChildMeasurementHint(
+ ChildMeasurementHint.values()[childMeasureHint]);
+
+ getWidget().updateSelectionProperties(uidl, getState(), isReadOnly());
+
+ if (uidl.hasAttribute("alb")) {
+ getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb");
+ } else {
+ // Need to clear the actions if the action handlers have been
+ // removed
+ getWidget().bodyActionKeys = null;
+ }
+
+ getWidget().setCacheRateFromUIDL(uidl);
+
+ getWidget().recalcWidths = uidl.hasAttribute("recalcWidths");
+ if (getWidget().recalcWidths) {
+ getWidget().tHead.clear();
+ getWidget().tFoot.clear();
+ }
+
+ getWidget().updatePageLength(uidl);
+
+ getWidget().updateFirstVisibleAndScrollIfNeeded(uidl);
+
+ getWidget().showRowHeaders = uidl.getBooleanAttribute("rowheaders");
+ getWidget().showColHeaders = uidl.getBooleanAttribute("colheaders");
+
+ getWidget().updateSortingProperties(uidl);
+
+ getWidget().updateActionMap(uidl);
+
+ getWidget().updateColumnProperties(uidl);
+
+ UIDL ac = uidl.getChildByTagName("-ac");
+ if (ac == null) {
+ if (getWidget().dropHandler != null) {
+ // remove dropHandler if not present anymore
+ getWidget().dropHandler = null;
+ }
+ } else {
+ if (getWidget().dropHandler == null) {
+ getWidget().dropHandler = getWidget().new VScrollTableDropHandler();
+ }
+ getWidget().dropHandler.updateAcceptRules(ac);
+ }
+
+ UIDL partialRowAdditions = uidl.getChildByTagName("prows");
+ UIDL partialRowUpdates = uidl.getChildByTagName("urows");
+ if (partialRowUpdates != null || partialRowAdditions != null) {
+ getWidget().postponeSanityCheckForLastRendered = true;
+ // we may have pending cache row fetch, cancel it. See #2136
+ getWidget().rowRequestHandler.cancel();
+
+ getWidget().updateRowsInBody(partialRowUpdates);
+ getWidget().addAndRemoveRows(partialRowAdditions);
+
+ // sanity check (in case the value has slipped beyond the total
+ // amount of rows)
+ getWidget().scrollBody.setLastRendered(getWidget().scrollBody
+ .getLastRendered());
+ getWidget().updateMaxIndent();
+ } else {
+ getWidget().postponeSanityCheckForLastRendered = false;
+ UIDL rowData = uidl.getChildByTagName("rows");
+ if (rowData != null) {
+ // we may have pending cache row fetch, cancel it. See #2136
+ getWidget().rowRequestHandler.cancel();
+
+ if (!getWidget().recalcWidths
+ && getWidget().initializedAndAttached) {
+ getWidget().updateBody(rowData,
+ uidl.getIntAttribute("firstrow"),
+ uidl.getIntAttribute("rows"));
+ if (getWidget().headerChangedDuringUpdate) {
+ getWidget().triggerLazyColumnAdjustment(true);
+ }
+ } else {
+ getWidget().initializeRows(uidl, rowData);
+ }
+ }
+ }
+
+ boolean keyboardSelectionOverRowFetchInProgress = getWidget()
+ .selectSelectedRows(uidl);
+
+ // If a row had an open context menu before the update, and after the
+ // update there's a row with the same key as that row, restore the
+ // context menu. See #8526.
+ showSavedContextMenu(contextMenuBeforeUpdate);
+
+ if (!getWidget().isSelectable()) {
+ getWidget().scrollBody.addStyleName(getWidget()
+ .getStylePrimaryName() + "-body-noselection");
+ } else {
+ getWidget().scrollBody.removeStyleName(getWidget()
+ .getStylePrimaryName() + "-body-noselection");
+ }
+
+ getWidget().hideScrollPositionAnnotation();
+
+ // selection is no in sync with server, avoid excessive server visits by
+ // clearing to flag used during the normal operation
+ if (!keyboardSelectionOverRowFetchInProgress) {
+ getWidget().selectionChanged = false;
+ }
+
+ /*
+ * This is called when the Home or page up button has been pressed in
+ * selectable mode and the next selected row was not yet rendered in the
+ * client
+ */
+ if (getWidget().selectFirstItemInNextRender
+ || getWidget().focusFirstItemInNextRender) {
+ getWidget().selectFirstRenderedRowInViewPort(
+ getWidget().focusFirstItemInNextRender);
+ getWidget().selectFirstItemInNextRender = getWidget().focusFirstItemInNextRender = false;
+ }
+
+ /*
+ * This is called when the page down or end button has been pressed in
+ * selectable mode and the next selected row was not yet rendered in the
+ * client
+ */
+ if (getWidget().selectLastItemInNextRender
+ || getWidget().focusLastItemInNextRender) {
+ getWidget().selectLastRenderedRowInViewPort(
+ getWidget().focusLastItemInNextRender);
+ getWidget().selectLastItemInNextRender = getWidget().focusLastItemInNextRender = false;
+ }
+ getWidget().multiselectPending = false;
+
+ if (getWidget().focusedRow != null) {
+ if (!getWidget().focusedRow.isAttached()
+ && !getWidget().rowRequestHandler.isRequestHandlerRunning()) {
+ // focused row has been orphaned, can't focus
+ if (getWidget().selectedRowKeys.contains(getWidget().focusedRow
+ .getKey())) {
+ // if row cache was refreshed, focused row should be
+ // in selection and exists with same index
+ getWidget().setRowFocus(
+ getWidget().getRenderedRowByKey(
+ getWidget().focusedRow.getKey()));
+ } else if (getWidget().selectedRowKeys.size() > 0) {
+ // try to focus any row in selection
+ getWidget().setRowFocus(
+ getWidget().getRenderedRowByKey(
+ getWidget().selectedRowKeys.iterator()
+ .next()));
+ } else {
+ // try to focus any row
+ getWidget().focusRowFromBody();
+ }
+ }
+ }
+
+ /*
+ * If the server has (re)initialized the rows, our selectionRangeStart
+ * row will point to an index that the server knows nothing about,
+ * causing problems if doing multi selection with shift. The field will
+ * be cleared a little later when the row focus has been restored.
+ * (#8584)
+ */
+ if (uidl.hasAttribute(TableConstants.ATTRIBUTE_KEY_MAPPER_RESET)
+ && uidl.getBooleanAttribute(TableConstants.ATTRIBUTE_KEY_MAPPER_RESET)
+ && getWidget().selectionRangeStart != null) {
+ assert !getWidget().selectionRangeStart.isAttached();
+ getWidget().selectionRangeStart = getWidget().focusedRow;
+ }
+
+ getWidget().tabIndex = getState().tabIndex;
+ getWidget().setProperTabIndex();
+
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ getWidget().resizeSortedColumnForSortIndicator();
+ }
+ });
+
+ // Remember this to detect situations where overflow hack might be
+ // needed during scrolling
+ getWidget().lastRenderedHeight = getWidget().scrollBody
+ .getOffsetHeight();
+
+ getWidget().rendering = false;
+ getWidget().headerChangedDuringUpdate = false;
+
+ getWidget().collapsibleMenuContent = getState().collapseMenuContent;
+ }
+
++ @Override
++ public void updateEnabledState(boolean enabledState) {
++ super.updateEnabledState(enabledState);
++ getWidget().enabled = isEnabled();
++
++ if (BrowserInfo.get().isIE8() && !getWidget().enabled) {
++ /*
++ * The disabled shim will not cover the table body if it is relative
++ * in IE8. See #7324
++ */
++ getWidget().scrollBodyPanel.getElement().getStyle()
++ .setPosition(Position.STATIC);
++ } else if (BrowserInfo.get().isIE8()) {
++ getWidget().scrollBodyPanel.getElement().getStyle()
++ .setPosition(Position.RELATIVE);
++ }
++
++ }
++
+ @Override
+ public VScrollTable getWidget() {
+ return (VScrollTable) super.getWidget();
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ // NOP, not rendered
+ }
+
+ @Override
+ public void layoutVertically() {
+ getWidget().updateHeight();
+ }
+
+ @Override
+ public void layoutHorizontally() {
+ getWidget().updateWidth();
+ }
+
+ @Override
+ public void postLayout() {
+ VScrollTable table = getWidget();
+ if (table.sizeNeedsInit) {
+ table.sizeInit();
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ // IE8 needs some hacks to measure sizes correctly
+ WidgetUtil.forceIE8Redraw(getWidget().getElement());
+
+ getLayoutManager().setNeedsMeasure(TableConnector.this);
+ ServerConnector parent = getParent();
+ if (parent instanceof ComponentConnector) {
+ getLayoutManager().setNeedsMeasure(
+ (ComponentConnector) parent);
+ }
+ getLayoutManager().setNeedsVerticalLayout(
+ TableConnector.this);
+ getLayoutManager().layoutNow();
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || getState().propertyReadOnly;
+ }
+
+ @Override
+ public TableState getState() {
+ return (TableState) super.getState();
+ }
+
+ /**
+ * Shows a saved row context menu if the row for the context menu is still
+ * visible. Does nothing if a context menu has not been saved.
+ *
+ * @param savedContextMenu
+ */
+ public void showSavedContextMenu(ContextMenuDetails savedContextMenu) {
+ if (isEnabled() && savedContextMenu != null) {
+ Iterator<Widget> iterator = getWidget().scrollBody.iterator();
+ while (iterator.hasNext()) {
+ Widget w = iterator.next();
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (row.getKey().equals(savedContextMenu.rowKey)) {
+ row.showContextMenu(savedContextMenu.left,
+ savedContextMenu.top);
+ }
+ }
+ }
+ }
+
+ @Override
+ public TooltipInfo getTooltipInfo(Element element) {
+
+ TooltipInfo info = null;
+
+ if (element != getWidget().getElement()) {
+ Object node = WidgetUtil.findWidget(element, VScrollTableRow.class);
+
+ if (node != null) {
+ VScrollTableRow row = (VScrollTableRow) node;
+ info = row.getTooltip(element);
+ }
+ }
+
+ if (info == null) {
+ info = super.getTooltipInfo(element);
+ }
+
+ return info;
+ }
+
+ @Override
+ public boolean hasTooltip() {
+ /*
+ * Tooltips for individual rows and cells are not processed until
+ * updateFromUIDL, so we can't be sure that there are no tooltips during
+ * onStateChange when this method is used.
+ */
+ return true;
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(
+ ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) {
+ // TODO Move code from updateFromUIDL to this method
+ }
+
+ @Override
+ protected void updateComponentSize(String newWidth, String newHeight) {
+ super.updateComponentSize(newWidth, newHeight);
+
+ if ("".equals(newWidth)) {
+ getWidget().updateWidth();
+ }
+ if ("".equals(newHeight)) {
+ getWidget().updateHeight();
+ }
+ }
+
+ @Override
+ public List<ComponentConnector> getChildComponents() {
+ if (childComponents == null) {
+ return Collections.emptyList();
+ }
+
+ return childComponents;
+ }
+
+ @Override
+ public void setChildComponents(List<ComponentConnector> childComponents) {
+ this.childComponents = childComponents;
+ }
+
+ @Override
+ public HandlerRegistration addConnectorHierarchyChangeHandler(
+ ConnectorHierarchyChangeHandler handler) {
+ return ensureHandlerManager().addHandler(
+ ConnectorHierarchyChangeEvent.TYPE, handler);
+ }
+
+ @Override
+ public void setChildMeasurementHint(ChildMeasurementHint hint) {
+ getWidget().setChildMeasurementHint(hint);
+ }
+
+ @Override
+ public ChildMeasurementHint getChildMeasurementHint() {
+ return getWidget().getChildMeasurementHint();
+ }
+
+}
--- /dev/null
- + toInt32(scrollPos)
+/*
+ * Copyright 2000-2014 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.client.widget.escalator;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerManager;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.Timer;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.widget.grid.events.ScrollEvent;
+import com.vaadin.client.widget.grid.events.ScrollHandler;
+
+/**
+ * An element-like bundle representing a configurable and visual scrollbar in
+ * one axis.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see VerticalScrollbarBundle
+ * @see HorizontalScrollbarBundle
+ */
+public abstract class ScrollbarBundle implements DeferredWorker {
+
+ private class ScrollEventFirer {
+ private final ScheduledCommand fireEventCommand = new ScheduledCommand() {
+ @Override
+ public void execute() {
+
+ /*
+ * Some kind of native-scroll-event related asynchronous problem
+ * occurs here (at least on desktops) where the internal
+ * bookkeeping isn't up to date with the real scroll position.
+ * The weird thing is, that happens only once, and if you drag
+ * scrollbar fast enough. After it has failed once, it never
+ * fails again.
+ *
+ * Theory: the user drags the scrollbar, and this command is
+ * executed before the browser has a chance to fire a scroll
+ * event (which normally would correct this situation). This
+ * would explain why slow scrolling doesn't trigger the problem,
+ * while fast scrolling does.
+ *
+ * To make absolutely sure that we have the latest scroll
+ * position, let's update the internal value.
+ *
+ * This might lead to a slight performance hit (on my computer
+ * it was never more than 3ms on either of Chrome 38 or Firefox
+ * 31). It also _slightly_ counteracts the purpose of the
+ * internal bookkeeping. But since getScrollPos is called 3
+ * times (on one direction) per scroll loop, it's still better
+ * to have take this small penalty than removing it altogether.
+ */
+ updateScrollPosFromDom();
+
+ getHandlerManager().fireEvent(new ScrollEvent());
+ isBeingFired = false;
+ }
+ };
+
+ private boolean isBeingFired;
+
+ public void scheduleEvent() {
+ if (!isBeingFired) {
+ /*
+ * We'll gather all the scroll events, and only fire once, once
+ * everything has calmed down.
+ */
+ Scheduler.get().scheduleDeferred(fireEventCommand);
+ isBeingFired = true;
+ }
+ }
+ }
+
+ /**
+ * The orientation of the scrollbar.
+ */
+ public enum Direction {
+ VERTICAL, HORIZONTAL;
+ }
+
+ private class TemporaryResizer {
+ private static final int TEMPORARY_RESIZE_DELAY = 1000;
+
+ private final Timer timer = new Timer() {
+ @Override
+ public void run() {
+ internalSetScrollbarThickness(1);
+ root.getStyle().setVisibility(Visibility.HIDDEN);
+ }
+ };
+
+ public void show() {
+ internalSetScrollbarThickness(OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX);
+ root.getStyle().setVisibility(Visibility.VISIBLE);
+ timer.schedule(TEMPORARY_RESIZE_DELAY);
+ }
+ }
+
+ /**
+ * A means to listen to when the scrollbar handle in a
+ * {@link ScrollbarBundle} either appears or is removed.
+ */
+ public interface VisibilityHandler extends EventHandler {
+ /**
+ * This method is called whenever the scrollbar handle's visibility is
+ * changed in a {@link ScrollbarBundle}.
+ *
+ * @param event
+ * the {@link VisibilityChangeEvent}
+ */
+ void visibilityChanged(VisibilityChangeEvent event);
+ }
+
+ public static class VisibilityChangeEvent extends
+ GwtEvent<VisibilityHandler> {
+ public static final Type<VisibilityHandler> TYPE = new Type<ScrollbarBundle.VisibilityHandler>() {
+ @Override
+ public String toString() {
+ return "VisibilityChangeEvent";
+ }
+ };
+
+ private final boolean isScrollerVisible;
+
+ private VisibilityChangeEvent(boolean isScrollerVisible) {
+ this.isScrollerVisible = isScrollerVisible;
+ }
+
+ /**
+ * Checks whether the scroll handle is currently visible or not
+ *
+ * @return <code>true</code> if the scroll handle is currently visible.
+ * <code>false</code> if not.
+ */
+ public boolean isScrollerVisible() {
+ return isScrollerVisible;
+ }
+
+ @Override
+ public Type<VisibilityHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(VisibilityHandler handler) {
+ handler.visibilityChanged(this);
+ }
+ }
+
+ /**
+ * The pixel size for OSX's invisible scrollbars.
+ * <p>
+ * Touch devices don't show a scrollbar at all, so the scrollbar size is
+ * irrelevant in their case. There doesn't seem to be any other popular
+ * platforms that has scrollbars similar to OSX. Thus, this behavior is
+ * tailored for OSX only, until additional platforms start behaving this
+ * way.
+ */
+ private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13;
+
+ /**
+ * A representation of a single vertical scrollbar.
+ *
+ * @see VerticalScrollbarBundle#getElement()
+ */
+ public final static class VerticalScrollbarBundle extends ScrollbarBundle {
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ root.addClassName(primaryStyleName + "-scroller-vertical");
+ }
+
+ @Override
+ protected void internalSetScrollPos(int px) {
+ root.setScrollTop(px);
+ }
+
+ @Override
+ protected int internalGetScrollPos() {
+ return root.getScrollTop();
+ }
+
+ @Override
+ protected void internalSetScrollSize(double px) {
+ scrollSizeElement.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ protected String internalGetScrollSize() {
+ return scrollSizeElement.getStyle().getHeight();
+ }
+
+ @Override
+ protected void internalSetOffsetSize(double px) {
+ root.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ public String internalGetOffsetSize() {
+ return root.getStyle().getHeight();
+ }
+
+ @Override
+ protected void internalSetScrollbarThickness(double px) {
+ root.getStyle().setPaddingRight(px, Unit.PX);
+ root.getStyle().setWidth(0, Unit.PX);
+ scrollSizeElement.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ protected String internalGetScrollbarThickness() {
+ return scrollSizeElement.getStyle().getWidth();
+ }
+
+ @Override
+ protected void internalForceScrollbar(boolean enable) {
+ if (enable) {
+ root.getStyle().setOverflowY(Overflow.SCROLL);
+ } else {
+ root.getStyle().clearOverflowY();
+ }
+ }
+
+ @Override
+ public Direction getDirection() {
+ return Direction.VERTICAL;
+ }
+ }
+
+ /**
+ * A representation of a single horizontal scrollbar.
+ *
+ * @see HorizontalScrollbarBundle#getElement()
+ */
+ public final static class HorizontalScrollbarBundle extends ScrollbarBundle {
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ root.addClassName(primaryStyleName + "-scroller-horizontal");
+ }
+
+ @Override
+ protected void internalSetScrollPos(int px) {
+ root.setScrollLeft(px);
+ }
+
+ @Override
+ protected int internalGetScrollPos() {
+ return root.getScrollLeft();
+ }
+
+ @Override
+ protected void internalSetScrollSize(double px) {
+ scrollSizeElement.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ protected String internalGetScrollSize() {
+ return scrollSizeElement.getStyle().getWidth();
+ }
+
+ @Override
+ protected void internalSetOffsetSize(double px) {
+ root.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ public String internalGetOffsetSize() {
+ return root.getStyle().getWidth();
+ }
+
+ @Override
+ protected void internalSetScrollbarThickness(double px) {
+ root.getStyle().setPaddingBottom(px, Unit.PX);
+ root.getStyle().setHeight(0, Unit.PX);
+ scrollSizeElement.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ protected String internalGetScrollbarThickness() {
+ return scrollSizeElement.getStyle().getHeight();
+ }
+
+ @Override
+ protected void internalForceScrollbar(boolean enable) {
+ if (enable) {
+ root.getStyle().setOverflowX(Overflow.SCROLL);
+ } else {
+ root.getStyle().clearOverflowX();
+ }
+ }
+
+ @Override
+ public Direction getDirection() {
+ return Direction.HORIZONTAL;
+ }
+ }
+
+ protected final Element root = DOM.createDiv();
+ protected final Element scrollSizeElement = DOM.createDiv();
+ protected boolean isInvisibleScrollbar = false;
+
+ private double scrollPos = 0;
+ private double maxScrollPos = 0;
+
+ private boolean scrollHandleIsVisible = false;
+
+ private boolean isLocked = false;
+
+ /** @deprecated access via {@link #getHandlerManager()} instead. */
+ @Deprecated
+ private HandlerManager handlerManager;
+
+ private TemporaryResizer invisibleScrollbarTemporaryResizer = new TemporaryResizer();
+
+ private final ScrollEventFirer scrollEventFirer = new ScrollEventFirer();
+
+ private HandlerRegistration scrollSizeTemporaryScrollHandler;
+ private HandlerRegistration offsetSizeTemporaryScrollHandler;
+
+ private ScrollbarBundle() {
+ root.appendChild(scrollSizeElement);
+ root.getStyle().setDisplay(Display.NONE);
+ root.setTabIndex(-1);
+ }
+
+ protected abstract String internalGetScrollSize();
+
+ /**
+ * Sets the primary style name
+ *
+ * @param primaryStyleName
+ * The primary style name to use
+ */
+ public void setStylePrimaryName(String primaryStyleName) {
+ root.setClassName(primaryStyleName + "-scroller");
+ }
+
+ /**
+ * Gets the root element of this scrollbar-composition.
+ *
+ * @return the root element
+ */
+ public final Element getElement() {
+ return root;
+ }
+
+ /**
+ * Modifies the scroll position of this scrollbar by a number of pixels.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param delta
+ * the delta in pixels to change the scroll position by
+ */
+ public final void setScrollPosByDelta(double delta) {
+ if (delta != 0) {
+ setScrollPos(getScrollPos() + delta);
+ }
+ }
+
+ /**
+ * Modifies {@link #root root's} dimensions in the axis the scrollbar is
+ * representing.
+ *
+ * @param px
+ * the new size of {@link #root} in the dimension this scrollbar
+ * is representing
+ */
+ protected abstract void internalSetOffsetSize(double px);
+
+ /**
+ * Sets the length of the scrollbar.
+ *
+ * @param px
+ * the length of the scrollbar in pixels
+ */
+ public final void setOffsetSize(final double px) {
+
+ /*
+ * This needs to be made step-by-step because IE8 flat-out refuses to
+ * fire a scroll event when the scroll size becomes smaller than the
+ * offset size. All other browser need to suffer alongside.
+ */
+
+ boolean newOffsetSizeIsGreaterThanScrollSize = px > getScrollSize();
+ boolean offsetSizeBecomesGreaterThanScrollSize = showsScrollHandle()
+ && newOffsetSizeIsGreaterThanScrollSize;
+ if (offsetSizeBecomesGreaterThanScrollSize && getScrollPos() != 0) {
+ // must be a field because Java insists.
+ offsetSizeTemporaryScrollHandler = addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ setOffsetSizeNow(px);
+ }
+ });
+ setScrollPos(0);
+ } else {
+ setOffsetSizeNow(px);
+ }
+ }
+
+ private void setOffsetSizeNow(double px) {
+ internalSetOffsetSize(Math.max(0, px));
+ recalculateMaxScrollPos();
+ forceScrollbar(showsScrollHandle());
+ fireVisibilityChangeIfNeeded();
+ if (offsetSizeTemporaryScrollHandler != null) {
+ offsetSizeTemporaryScrollHandler.removeHandler();
+ offsetSizeTemporaryScrollHandler = null;
+ }
+ }
+
+ /**
+ * Force the scrollbar to be visible with CSS. In practice, this means to
+ * set either <code>overflow-x</code> or <code>overflow-y</code> to "
+ * <code>scroll</code>" in the scrollbar's direction.
+ * <p>
+ * This is an IE8 workaround, since it doesn't always show scrollbars with
+ * <code>overflow: auto</code> enabled.
+ */
+ protected void forceScrollbar(boolean enable) {
+ if (enable) {
+ root.getStyle().clearDisplay();
+ } else {
+ root.getStyle().setDisplay(Display.NONE);
+ }
+ internalForceScrollbar(enable);
+ }
+
+ protected abstract void internalForceScrollbar(boolean enable);
+
+ /**
+ * Gets the length of the scrollbar
+ *
+ * @return the length of the scrollbar in pixels
+ */
+ public double getOffsetSize() {
+ return parseCssDimensionToPixels(internalGetOffsetSize());
+ }
+
+ public abstract String internalGetOffsetSize();
+
+ /**
+ * Sets the scroll position of the scrollbar in the axis the scrollbar is
+ * representing.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param px
+ * the new scroll position in pixels
+ */
+ public final void setScrollPos(double px) {
+ if (isLocked()) {
+ return;
+ }
+
+ double oldScrollPos = scrollPos;
+ scrollPos = Math.max(0, Math.min(maxScrollPos, truncate(px)));
+
+ if (!WidgetUtil.pixelValuesEqual(oldScrollPos, scrollPos)) {
+ if (isInvisibleScrollbar) {
+ invisibleScrollbarTemporaryResizer.show();
+ }
+
+ /*
+ * This is where the value needs to be converted into an integer no
+ * matter how we flip it, since GWT expects an integer value.
+ * There's no point making a JSNI method that accepts doubles as the
+ * scroll position, since the browsers themselves don't support such
+ * large numbers (as of today, 25.3.2014). This double-ranged is
+ * only facilitating future virtual scrollbars.
+ */
+ internalSetScrollPos(toInt32(scrollPos));
+ }
+ }
+
+ /**
+ * Should be called whenever this bundle is attached to the DOM (typically,
+ * from the onLoad of the containing widget). Used to ensure the DOM scroll
+ * position is maintained when detaching and reattaching the bundle.
+ *
+ * @since 7.4.1
+ */
+ public void onLoad() {
+ internalSetScrollPos(toInt32(scrollPos));
+ }
+
+ /**
+ * Truncates a double such that no decimal places are retained.
+ * <p>
+ * E.g. {@code trunc(2.3d) == 2.0d} and {@code trunc(-2.3d) == -2.0d}.
+ *
+ * @param num
+ * the double value to be truncated
+ * @return the {@code num} value without any decimal digits
+ */
+ private static double truncate(double num) {
+ if (num > 0) {
+ return Math.floor(num);
+ } else {
+ return Math.ceil(num);
+ }
+ }
+
+ /**
+ * Modifies the element's scroll position (scrollTop or scrollLeft).
+ * <p>
+ * <em>Note:</em> The parameter here is a type of integer (instead of a
+ * double) by design. The browsers internally convert all double values into
+ * an integer value. To make this fact explicit, this API has chosen to
+ * force integers already at this level.
+ *
+ * @param px
+ * integer pixel value to scroll to
+ */
+ protected abstract void internalSetScrollPos(int px);
+
+ /**
+ * Gets the scroll position of the scrollbar in the axis the scrollbar is
+ * representing.
+ *
+ * @return the new scroll position in pixels
+ */
+ public final double getScrollPos() {
+ assert internalGetScrollPos() == toInt32(scrollPos) : "calculated scroll position ("
- return val | 0;
++ + scrollPos
+ + ") did not match the DOM element scroll position ("
+ + internalGetScrollPos() + ")";
+ return scrollPos;
+ }
+
+ /**
+ * Retrieves the element's scroll position (scrollTop or scrollLeft).
+ * <p>
+ * <em>Note:</em> The parameter here is a type of integer (instead of a
+ * double) by design. The browsers internally convert all double values into
+ * an integer value. To make this fact explicit, this API has chosen to
+ * force integers already at this level.
+ *
+ * @return integer pixel value of the scroll position
+ */
+ protected abstract int internalGetScrollPos();
+
+ /**
+ * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in
+ * such a way that the scrollbar is able to scroll a certain number of
+ * pixels in the axis it is representing.
+ *
+ * @param px
+ * the new size of {@link #scrollSizeElement} in the dimension
+ * this scrollbar is representing
+ */
+ protected abstract void internalSetScrollSize(double px);
+
+ /**
+ * Sets the amount of pixels the scrollbar needs to be able to scroll
+ * through.
+ *
+ * @param px
+ * the number of pixels the scrollbar should be able to scroll
+ * through
+ */
+ public final void setScrollSize(final double px) {
+
+ /*
+ * This needs to be made step-by-step because IE8 flat-out refuses to
+ * fire a scroll event when the scroll size becomes smaller than the
+ * offset size. All other browser need to suffer alongside.
+ */
+
+ boolean newScrollSizeIsSmallerThanOffsetSize = px <= getOffsetSize();
+ boolean scrollSizeBecomesSmallerThanOffsetSize = showsScrollHandle()
+ && newScrollSizeIsSmallerThanOffsetSize;
+ if (scrollSizeBecomesSmallerThanOffsetSize && getScrollPos() != 0) {
+ // must be a field because Java insists.
+ scrollSizeTemporaryScrollHandler = addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ setScrollSizeNow(px);
+ }
+ });
+ setScrollPos(0);
+ } else {
+ setScrollSizeNow(px);
+ }
+ }
+
+ private void setScrollSizeNow(double px) {
+ internalSetScrollSize(Math.max(0, px));
+ recalculateMaxScrollPos();
+ forceScrollbar(showsScrollHandle());
+ fireVisibilityChangeIfNeeded();
+ if (scrollSizeTemporaryScrollHandler != null) {
+ scrollSizeTemporaryScrollHandler.removeHandler();
+ scrollSizeTemporaryScrollHandler = null;
+ }
+ }
+
+ /**
+ * Gets the amount of pixels the scrollbar needs to be able to scroll
+ * through.
+ *
+ * @return the number of pixels the scrollbar should be able to scroll
+ * through
+ */
+ public double getScrollSize() {
+ return parseCssDimensionToPixels(internalGetScrollSize());
+ }
+
+ /**
+ * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the
+ * opposite axis to what the scrollbar is representing.
+ *
+ * @param px
+ * the dimension that {@link #scrollSizeElement} should take in
+ * the opposite axis to what the scrollbar is representing
+ */
+ protected abstract void internalSetScrollbarThickness(double px);
+
+ /**
+ * Sets the scrollbar's thickness.
+ * <p>
+ * If the thickness is set to 0, the scrollbar will be treated as an
+ * "invisible" scrollbar. This means, the DOM structure will be given a
+ * non-zero size, but {@link #getScrollbarThickness()} will still return the
+ * value 0.
+ *
+ * @param px
+ * the scrollbar's thickness in pixels
+ */
+ public final void setScrollbarThickness(double px) {
+ isInvisibleScrollbar = (px == 0);
+
+ if (isInvisibleScrollbar) {
+ Event.sinkEvents(root, Event.ONSCROLL);
+ Event.setEventListener(root, new EventListener() {
+ @Override
+ public void onBrowserEvent(Event event) {
+ invisibleScrollbarTemporaryResizer.show();
+ }
+ });
+ root.getStyle().setVisibility(Visibility.HIDDEN);
+ } else {
+ Event.sinkEvents(root, 0);
+ Event.setEventListener(root, null);
+ root.getStyle().clearVisibility();
+ }
+
+ internalSetScrollbarThickness(Math.max(1d, px));
+ }
+
+ /**
+ * Gets the scrollbar's thickness as defined in the DOM.
+ *
+ * @return the scrollbar's thickness as defined in the DOM, in pixels
+ */
+ protected abstract String internalGetScrollbarThickness();
+
+ /**
+ * Gets the scrollbar's thickness.
+ * <p>
+ * This value will differ from the value in the DOM, if the thickness was
+ * set to 0 with {@link #setScrollbarThickness(double)}, as the scrollbar is
+ * then treated as "invisible."
+ *
+ * @return the scrollbar's thickness in pixels
+ */
+ public final double getScrollbarThickness() {
+ if (!isInvisibleScrollbar) {
+ return parseCssDimensionToPixels(internalGetScrollbarThickness());
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Checks whether the scrollbar's handle is visible.
+ * <p>
+ * In other words, this method checks whether the contents is larger than
+ * can visually fit in the element.
+ *
+ * @return <code>true</code> iff the scrollbar's handle is visible
+ */
+ public boolean showsScrollHandle() {
+ return getScrollSize() - getOffsetSize() > WidgetUtil.PIXEL_EPSILON;
+ }
+
+ public void recalculateMaxScrollPos() {
+ double scrollSize = getScrollSize();
+ double offsetSize = getOffsetSize();
+ maxScrollPos = Math.max(0, scrollSize - offsetSize);
+
+ // make sure that the correct max scroll position is maintained.
+ setScrollPos(scrollPos);
+ }
+
+ /**
+ * This is a method that JSNI can call to synchronize the object state from
+ * the DOM.
+ */
+ private final void updateScrollPosFromDom() {
+
+ /*
+ * TODO: this method probably shouldn't be called from Escalator's JSNI,
+ * but probably could be handled internally by this listening to its own
+ * element. Would clean up the code quite a bit. Needs further
+ * investigation.
+ */
+
+ int newScrollPos = internalGetScrollPos();
+ if (!isLocked()) {
+ scrollPos = newScrollPos;
+ scrollEventFirer.scheduleEvent();
+ } else if (scrollPos != newScrollPos) {
+ // we need to actually undo the setting of the scroll.
+ internalSetScrollPos(toInt32(scrollPos));
+ }
+ }
+
+ protected HandlerManager getHandlerManager() {
+ if (handlerManager == null) {
+ handlerManager = new HandlerManager(this);
+ }
+ return handlerManager;
+ }
+
+ /**
+ * Adds handler for the scrollbar handle visibility.
+ *
+ * @param handler
+ * the {@link VisibilityHandler} to add
+ * @return {@link HandlerRegistration} used to remove the handler
+ */
+ public HandlerRegistration addVisibilityHandler(
+ final VisibilityHandler handler) {
+ return getHandlerManager().addHandler(VisibilityChangeEvent.TYPE,
+ handler);
+ }
+
+ private void fireVisibilityChangeIfNeeded() {
+ final boolean oldHandleIsVisible = scrollHandleIsVisible;
+ scrollHandleIsVisible = showsScrollHandle();
+ if (oldHandleIsVisible != scrollHandleIsVisible) {
+ final VisibilityChangeEvent event = new VisibilityChangeEvent(
+ scrollHandleIsVisible);
+ getHandlerManager().fireEvent(event);
+ }
+ }
+
+ /**
+ * Converts a double into an integer by JavaScript's terms.
+ * <p>
+ * Implementation copied from {@link Element#toInt32(double)}.
+ *
+ * @param val
+ * the double value to convert into an integer
+ * @return the double value converted to an integer
+ */
+ private static native int toInt32(double val)
+ /*-{
++ return Math.round(val) | 0;
+ }-*/;
+
+ /**
+ * Locks or unlocks the scrollbar bundle.
+ * <p>
+ * A locked scrollbar bundle will refuse to scroll, both programmatically
+ * and via user-triggered events.
+ *
+ * @param isLocked
+ * <code>true</code> to lock, <code>false</code> to unlock
+ */
+ public void setLocked(boolean isLocked) {
+ this.isLocked = isLocked;
+ }
+
+ /**
+ * Checks whether the scrollbar bundle is locked or not.
+ *
+ * @return <code>true</code> iff the scrollbar bundle is locked
+ */
+ public boolean isLocked() {
+ return isLocked;
+ }
+
+ /**
+ * Returns the scroll direction of this scrollbar bundle.
+ *
+ * @return the scroll direction of this scrollbar bundle
+ */
+ public abstract Direction getDirection();
+
+ /**
+ * Adds a scroll handler to the scrollbar bundle.
+ *
+ * @param handler
+ * the handler to add
+ * @return the registration object for the handler registration
+ */
+ public HandlerRegistration addScrollHandler(final ScrollHandler handler) {
+ return getHandlerManager().addHandler(ScrollEvent.TYPE, handler);
+ }
+
+ private static double parseCssDimensionToPixels(String size) {
+
+ /*
+ * Sizes of elements are calculated from CSS rather than
+ * element.getOffset*() because those values are 0 whenever display:
+ * none. Because we know that all elements have populated
+ * CSS-dimensions, it's better to do it that way.
+ *
+ * Another solution would be to make the elements visible while
+ * measuring and then re-hide them, but that would cause unnecessary
+ * reflows that would probably kill the performance dead.
+ */
+
+ if (size.isEmpty()) {
+ return 0;
+ } else {
+ assert size.endsWith("px") : "Can't parse CSS dimension \"" + size
+ + "\"";
+ return Double.parseDouble(size.substring(0, size.length() - 2));
+ }
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return scrollSizeTemporaryScrollHandler != null
+ || offsetSizeTemporaryScrollHandler != null;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.client.widgets;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gwt.animation.client.Animation;
+import com.google.gwt.animation.client.AnimationScheduler;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+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.Unit;
+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.shared.HandlerRegistration;
+import com.google.gwt.logging.client.LogConfiguration;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.RequiresResize;
+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.client.BrowserInfo;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.Profiler;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.SubPartAware;
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widget.escalator.ColumnConfiguration;
+import com.vaadin.client.widget.escalator.EscalatorUpdater;
+import com.vaadin.client.widget.escalator.FlyweightCell;
+import com.vaadin.client.widget.escalator.FlyweightRow;
+import com.vaadin.client.widget.escalator.PositionFunction;
+import com.vaadin.client.widget.escalator.PositionFunction.AbsolutePosition;
+import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition;
+import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition;
+import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition;
+import com.vaadin.client.widget.escalator.Row;
+import com.vaadin.client.widget.escalator.RowContainer;
+import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer;
+import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent;
+import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
+import com.vaadin.client.widget.escalator.ScrollbarBundle;
+import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle;
+import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle;
+import com.vaadin.client.widget.escalator.Spacer;
+import com.vaadin.client.widget.escalator.SpacerUpdater;
+import com.vaadin.client.widget.grid.events.ScrollEvent;
+import com.vaadin.client.widget.grid.events.ScrollHandler;
+import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.util.SharedUtil;
+
+/*-
+
+ Maintenance Notes! Reading these might save your day.
+ (note for editors: line width is 80 chars, including the
+ one-space indentation)
+
+
+ == Row Container Structure
+
+ AbstractRowContainer
+ |-- AbstractStaticRowContainer
+ | |-- HeaderRowContainer
+ | `-- FooterContainer
+ `---- BodyRowContainerImpl
+
+ AbstractRowContainer is intended to contain all common logic
+ between RowContainers. It manages the bookkeeping of row
+ count, makes sure that all individual cells are rendered
+ the same way, and so on.
+
+ AbstractStaticRowContainer has some special logic that is
+ required by all RowContainers that don't scroll (hence the
+ word "static"). HeaderRowContainer and FooterRowContainer
+ are pretty thin special cases of a StaticRowContainer
+ (mostly relating to positioning of the root element).
+
+ BodyRowContainerImpl could also be split into an additional
+ "AbstractScrollingRowContainer", but I felt that no more
+ inner classes were needed. So it contains both logic
+ required for making things scroll about, and equivalent
+ special cases for layouting, as are found in
+ Header/FooterRowContainers.
+
+
+ == The Three Indices
+
+ Each RowContainer can be thought to have three levels of
+ indices for any given displayed row (but the distinction
+ matters primarily for the BodyRowContainerImpl, because of
+ the way it scrolls through data):
+
+ - Logical index
+ - Physical (or DOM) index
+ - Visual index
+
+ LOGICAL INDEX is the index that is linked to the data
+ source. If you want your data source to represent a SQL
+ database with 10 000 rows, the 7 000:th row in the SQL has a
+ logical index of 6 999, since the index is 0-based (unless
+ that data source does some funky logic).
+
+ PHYSICAL INDEX is the index for a row that you see in a
+ browser's DOM inspector. If your row is the second <tr>
+ element within a <tbody> tag, it has a physical index of 1
+ (because of 0-based indices). In Header and
+ FooterRowContainers, you are safe to assume that the logical
+ index is the same as the physical index. But because the
+ BodyRowContainerImpl never displays large data sources
+ entirely in the DOM, a physical index usually has no
+ apparent direct relationship with its logical index.
+
+ VISUAL INDEX is the index relating to the order that you
+ see a row in, in the browser, as it is rendered. The
+ topmost row is 0, the second is 1, and so on. The visual
+ index is similar to the physical index in the sense that
+ Header and FooterRowContainers can assume a 1:1
+ relationship between visual index and logical index. And
+ again, BodyRowContainerImpl has no such relationship. The
+ body's visual index has additionally no apparent
+ relationship with its physical index. Because the <tr> tags
+ are reused in the body and visually repositioned with CSS
+ as the user scrolls, the relationship between physical
+ index and visual index is quickly broken. You can get an
+ element's visual index via the field
+ BodyRowContainerImpl.visualRowOrder.
+
+ Currently, the physical and visual indices are kept in sync
+ _most of the time_ by a deferred rearrangement of rows.
+ They become desynced when scrolling. This is to help screen
+ readers to read the contents from the DOM in a natural
+ order. See BodyRowContainerImpl.DeferredDomSorter for more
+ about that.
+
+ */
+
+/**
+ * A workaround-class for GWT and JSNI.
+ * <p>
+ * GWT is unable to handle some method calls to Java methods in inner-classes
+ * from within JSNI blocks. Having that inner class extend a non-inner-class (or
+ * implement such an interface), makes it possible for JSNI to indirectly refer
+ * to the inner class, by invoking methods and fields in the non-inner-class
+ * API.
+ *
+ * @see Escalator.Scroller
+ */
+abstract class JsniWorkaround {
+ /**
+ * A JavaScript function that handles the scroll DOM event, and passes it on
+ * to Java code.
+ *
+ * @see #createScrollListenerFunction(Escalator)
+ * @see Escalator#onScroll()
+ * @see Escalator.Scroller#onScroll()
+ */
+ protected final JavaScriptObject scrollListenerFunction;
+
+ /**
+ * A JavaScript function that handles the mousewheel DOM event, and passes
+ * it on to Java code.
+ *
+ * @see #createMousewheelListenerFunction(Escalator)
+ * @see Escalator#onScroll()
+ * @see Escalator.Scroller#onScroll()
+ */
+ protected final JavaScriptObject mousewheelListenerFunction;
+
+ /**
+ * A JavaScript function that handles the touch start DOM event, and passes
+ * it on to Java code.
+ *
+ * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchStartFunction;
+
+ /**
+ * A JavaScript function that handles the touch move DOM event, and passes
+ * it on to Java code.
+ *
+ * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchMoveFunction;
+
+ /**
+ * A JavaScript function that handles the touch end and cancel DOM events,
+ * and passes them on to Java code.
+ *
+ * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchEndFunction;
+
+ protected TouchHandlerBundle touchHandlerBundle;
+
+ protected JsniWorkaround(final Escalator escalator) {
+ scrollListenerFunction = createScrollListenerFunction(escalator);
+ mousewheelListenerFunction = createMousewheelListenerFunction(escalator);
+
+ touchHandlerBundle = new TouchHandlerBundle(escalator);
+ touchStartFunction = touchHandlerBundle.getTouchStartHandler();
+ touchMoveFunction = touchHandlerBundle.getTouchMoveHandler();
+ touchEndFunction = touchHandlerBundle.getTouchEndHandler();
+ }
+
+ /**
+ * A method that constructs the JavaScript function that will be stored into
+ * {@link #scrollListenerFunction}.
+ *
+ * @param esc
+ * a reference to the current instance of {@link Escalator}
+ * @see Escalator#onScroll()
+ */
+ protected abstract JavaScriptObject createScrollListenerFunction(
+ Escalator esc);
+
+ /**
+ * A method that constructs the JavaScript function that will be stored into
+ * {@link #mousewheelListenerFunction}.
+ *
+ * @param esc
+ * a reference to the current instance of {@link Escalator}
+ * @see Escalator#onScroll()
+ */
+ protected abstract JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc);
+}
+
+/**
+ * A low-level table-like widget that features a scrolling virtual viewport and
+ * lazily generated rows.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Escalator extends Widget implements RequiresResize,
+ DeferredWorker, SubPartAware {
+
+ // todo comments legend
+ /*
+ * [[optimize]]: There's an opportunity to rewrite the code in such a way
+ * that it _might_ perform better (rememeber to measure, implement,
+ * re-measure)
+ */
+ /*
+ * [[mpixscroll]]: This code will require alterations that are relevant for
+ * supporting the scrolling through more pixels than some browsers normally
+ * would support. (i.e. when we support more than "a million" pixels in the
+ * escalator DOM). NOTE: these bits can most often also be identified by
+ * searching for code that call scrollElem.getScrollTop();.
+ */
+ /*
+ * [[spacer]]: Code that is important to make spacers work.
+ */
+
+ /**
+ * A utility class that contains utility methods that are usually called
+ * from JSNI.
+ * <p>
+ * The methods are moved in this class to minimize the amount of JSNI code
+ * as much as feasible.
+ */
+ static class JsniUtil {
+ public static class TouchHandlerBundle {
+
+ /**
+ * A <a href=
+ * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html"
+ * >JavaScriptObject overlay</a> for the <a
+ * href="http://www.w3.org/TR/touch-events/">JavaScript
+ * TouchEvent</a> object.
+ * <p>
+ * This needs to be used in the touch event handlers, since GWT's
+ * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
+ * can't be cast from the JSNI call, and the
+ * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
+ * properly populated with the correct values.
+ */
+ private final static class CustomTouchEvent extends
+ JavaScriptObject {
+ protected CustomTouchEvent() {
+ }
+
+ public native NativeEvent getNativeEvent()
+ /*-{
+ return this;
+ }-*/;
+
+ public native int getPageX()
+ /*-{
+ return this.targetTouches[0].pageX;
+ }-*/;
+
+ public native int getPageY()
+ /*-{
+ return this.targetTouches[0].pageY;
+ }-*/;
+ }
+
+ private final Escalator escalator;
+
+ public TouchHandlerBundle(final Escalator escalator) {
+ this.escalator = escalator;
+ }
+
+ public native JavaScriptObject getTouchStartHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
+ });
+ }-*/;
+
+ public native JavaScriptObject getTouchMoveHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
+ });
+ }-*/;
+
+ public native JavaScriptObject getTouchEndHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
+ });
+ }-*/;
+
+ // Duration of the inertial scrolling simulation. Devices with
+ // larger screens take longer durations.
+ private static final int DURATION = Window.getClientHeight();
+ // multiply scroll velocity with repeated touching
+ private int acceleration = 1;
+ private boolean touching = false;
+ // Two movement objects for storing status and processing touches
+ private Movement yMov, xMov;
+ final double MIN_VEL = 0.6, MAX_VEL = 4, F_VEL = 1500, F_ACC = 0.7,
+ F_AXIS = 1;
+
+ // The object to deal with one direction scrolling
+ private class Movement {
+ final List<Double> speeds = new ArrayList<Double>();
+ final ScrollbarBundle scroll;
+ double position, offset, velocity, prevPos, prevTime, delta;
+ boolean run, vertical;
+
+ public Movement(boolean vertical) {
+ this.vertical = vertical;
+ scroll = vertical ? escalator.verticalScrollbar
+ : escalator.horizontalScrollbar;
+ }
+
+ public void startTouch(CustomTouchEvent event) {
+ speeds.clear();
+ prevPos = pagePosition(event);
+ prevTime = Duration.currentTimeMillis();
+ }
+
+ public void moveTouch(CustomTouchEvent event) {
+ double pagePosition = pagePosition(event);
+ if (pagePosition > -1) {
+ delta = prevPos - pagePosition;
+ double now = Duration.currentTimeMillis();
+ double ellapsed = now - prevTime;
+ velocity = delta / ellapsed;
+ // if last speed was so low, reset speeds and start
+ // storing again
+ if (speeds.size() > 0 && !validSpeed(speeds.get(0))) {
+ speeds.clear();
+ run = true;
+ }
+ speeds.add(0, velocity);
+ prevTime = now;
+ prevPos = pagePosition;
+ }
+ }
+
+ public void endTouch(CustomTouchEvent event) {
+ // Compute average speed
+ velocity = 0;
+ for (double s : speeds) {
+ velocity += s / speeds.size();
+ }
+ position = scroll.getScrollPos();
+
+ // Compute offset, and adjust it with an easing curve so as
+ // movement is smoother.
+ offset = F_VEL * velocity * acceleration
+ * easingInOutCos(velocity, MAX_VEL);
+
+ // Enable or disable inertia movement in this axis
+ run = validSpeed(velocity);
+ if (run) {
+ event.getNativeEvent().preventDefault();
+ }
+ }
+
+ void validate(Movement other) {
+ if (!run || other.velocity > 0
+ && Math.abs(velocity / other.velocity) < F_AXIS) {
+ delta = offset = 0;
+ run = false;
+ }
+ }
+
+ void stepAnimation(double progress) {
+ scroll.setScrollPos(position + offset * progress);
+ }
+
+ int pagePosition(CustomTouchEvent event) {
+ JsArray<Touch> a = event.getNativeEvent().getTouches();
+ return vertical ? a.get(0).getPageY() : a.get(0).getPageX();
+ }
+
+ boolean validSpeed(double speed) {
+ return Math.abs(speed) > MIN_VEL;
+ }
+ }
+
+ // Using GWT animations which take care of native animation frames.
+ private Animation animation = new Animation() {
+ @Override
+ public void onUpdate(double progress) {
+ xMov.stepAnimation(progress);
+ yMov.stepAnimation(progress);
+ }
+
+ @Override
+ public double interpolate(double progress) {
+ return easingOutCirc(progress);
+ };
+
+ @Override
+ public void onComplete() {
+ touching = false;
+ escalator.body.domSorter.reschedule();
+ };
+
+ @Override
+ public void run(int duration) {
+ if (xMov.run || yMov.run) {
+ super.run(duration);
+ } else {
+ onComplete();
+ }
+ };
+ };
+
+ public void touchStart(final CustomTouchEvent event) {
+ if (event.getNativeEvent().getTouches().length() == 1) {
+ if (yMov == null) {
+ yMov = new Movement(true);
+ xMov = new Movement(false);
+ }
+ if (animation.isRunning()) {
+ acceleration += F_ACC;
+ event.getNativeEvent().preventDefault();
+ animation.cancel();
+ } else {
+ acceleration = 1;
+ }
+ xMov.startTouch(event);
+ yMov.startTouch(event);
+ touching = true;
+ } else {
+ touching = false;
+ animation.cancel();
+ acceleration = 1;
+ }
+ }
+
+ public void touchMove(final CustomTouchEvent event) {
+ if (touching) {
+ xMov.moveTouch(event);
+ yMov.moveTouch(event);
+ xMov.validate(yMov);
+ yMov.validate(xMov);
+ event.getNativeEvent().preventDefault();
+ moveScrollFromEvent(escalator, xMov.delta, yMov.delta,
+ event.getNativeEvent());
+ }
+ }
+
+ public void touchEnd(final CustomTouchEvent event) {
+ if (touching) {
+ xMov.endTouch(event);
+ yMov.endTouch(event);
+ xMov.validate(yMov);
+ yMov.validate(xMov);
+ // Adjust duration so as longer movements take more duration
+ boolean vert = !xMov.run || yMov.run
+ && Math.abs(yMov.offset) > Math.abs(xMov.offset);
+ double delta = Math.abs((vert ? yMov : xMov).offset);
+ animation.run((int) (3 * DURATION * easingOutExp(delta)));
+ }
+ }
+
+ private double easingInOutCos(double val, double max) {
+ return 0.5 - 0.5 * Math.cos(Math.PI * Math.signum(val)
+ * Math.min(Math.abs(val), max) / max);
+ }
+
+ private double easingOutExp(double delta) {
+ return (1 - Math.pow(2, -delta / 1000));
+ }
+
+ private double easingOutCirc(double progress) {
+ return Math.sqrt(1 - (progress - 1) * (progress - 1));
+ }
+ }
+
+ public static void moveScrollFromEvent(final Escalator escalator,
+ final double deltaX, final double deltaY,
+ final NativeEvent event) {
+
+ if (!Double.isNaN(deltaX)) {
+ escalator.horizontalScrollbar.setScrollPosByDelta(deltaX);
+ }
+
+ if (!Double.isNaN(deltaY)) {
+ escalator.verticalScrollbar.setScrollPosByDelta(deltaY);
+ }
+
+ /*
+ * TODO: only prevent if not scrolled to end/bottom. Or no? UX team
+ * needs to decide.
+ */
+ final boolean warrantedYScroll = deltaY != 0
+ && escalator.verticalScrollbar.showsScrollHandle();
+ final boolean warrantedXScroll = deltaX != 0
+ && escalator.horizontalScrollbar.showsScrollHandle();
+ if (warrantedYScroll || warrantedXScroll) {
+ event.preventDefault();
+ }
+ }
+ }
+
+ /**
+ * ScrollDestination case-specific handling logic.
+ */
+ private static double getScrollPos(final ScrollDestination destination,
+ final double targetStartPx, final double targetEndPx,
+ final double viewportStartPx, final double viewportEndPx,
+ final double padding) {
+
+ final double viewportLength = viewportEndPx - viewportStartPx;
+
+ switch (destination) {
+
+ /*
+ * Scroll as little as possible to show the target element. If the
+ * element fits into view, this works as START or END depending on the
+ * current scroll position. If the element does not fit into view, this
+ * works as START.
+ */
+ case ANY: {
+ final double startScrollPos = targetStartPx - padding;
+ final double endScrollPos = targetEndPx + padding - viewportLength;
+
+ if (startScrollPos < viewportStartPx) {
+ return startScrollPos;
+ } else if (targetEndPx + padding > viewportEndPx) {
+ return endScrollPos;
+ } else {
+ // NOOP, it's already visible
+ return viewportStartPx;
+ }
+ }
+
+ /*
+ * Scrolls so that the element is shown at the end of the viewport. The
+ * viewport will, however, not scroll before its first element.
+ */
+ case END: {
+ return targetEndPx + padding - viewportLength;
+ }
+
+ /*
+ * Scrolls so that the element is shown in the middle of the viewport.
+ * The viewport will, however, not scroll beyond its contents, given
+ * more elements than what the viewport is able to show at once. Under
+ * no circumstances will the viewport scroll before its first element.
+ */
+ case MIDDLE: {
+ final double targetMiddle = targetStartPx
+ + (targetEndPx - targetStartPx) / 2;
+ return targetMiddle - viewportLength / 2;
+ }
+
+ /*
+ * Scrolls so that the element is shown at the start of the viewport.
+ * The viewport will, however, not scroll beyond its contents.
+ */
+ case START: {
+ return targetStartPx - padding;
+ }
+
+ /*
+ * Throw an error if we're here. This can only mean that
+ * ScrollDestination has been carelessly amended..
+ */
+ default: {
+ throw new IllegalArgumentException(
+ "Internal: ScrollDestination has been modified, "
+ + "but Escalator.getScrollPos has not been updated "
+ + "to match new values.");
+ }
+ }
+
+ }
+
+ /** An inner class that handles all logic related to scrolling. */
+ private class Scroller extends JsniWorkaround {
+ private double lastScrollTop = 0;
+ private double lastScrollLeft = 0;
+
+ public Scroller() {
+ super(Escalator.this);
+ }
+
+ @Override
+ protected native JavaScriptObject createScrollListenerFunction(
+ Escalator esc)
+ /*-{
+ var vScroll = esc.@com.vaadin.client.widgets.Escalator::verticalScrollbar;
+ var vScrollElem = vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
+
+ var hScroll = esc.@com.vaadin.client.widgets.Escalator::horizontalScrollbar;
+ var hScrollElem = hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
+
+ return $entry(function(e) {
+ var target = e.target || e.srcElement; // IE8 uses e.scrElement
+
+ // in case the scroll event was native (i.e. scrollbars were dragged, or
+ // the scrollTop/Left was manually modified), the bundles have old cache
+ // values. We need to make sure that the caches are kept up to date.
+ if (target === vScrollElem) {
+ vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
+ } else if (target === hScrollElem) {
+ hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
+ } else {
+ $wnd.console.error("unexpected scroll target: "+target);
+ }
+ });
+ }-*/;
+
+ @Override
+ protected native JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc)
+ /*-{
+ return $entry(function(e) {
+ var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
+ var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
+
+ // Delta mode 0 is in pixels; we don't need to do anything...
+
+ // A delta mode of 1 means we're scrolling by lines instead of pixels
+ // We need to scale the number of lines by the default line height
+ if(e.deltaMode === 1) {
+ var brc = esc.@com.vaadin.client.widgets.Escalator::body;
+ deltaY *= brc.@com.vaadin.client.widgets.Escalator.AbstractRowContainer::getDefaultRowHeight()();
+ }
+
+ // Other delta modes aren't supported
+ if((e.deltaMode !== undefined) && (e.deltaMode >= 2 || e.deltaMode < 0)) {
+ var msg = "Unsupported wheel delta mode \"" + e.deltaMode + "\"";
+
+ // Print warning message
+ esc.@com.vaadin.client.widgets.Escalator::logWarning(*)(msg);
+ }
+
+ // IE8 has only delta y
+ if (isNaN(deltaY)) {
+ deltaY = -0.5*e.wheelDelta;
+ }
+
+ @com.vaadin.client.widgets.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
+ });
+ }-*/;
+
+ /**
+ * Recalculates the virtual viewport represented by the scrollbars, so
+ * that the sizes of the scroll handles appear correct in the browser
+ */
+ public void recalculateScrollbarsForVirtualViewport() {
+ double scrollContentHeight = body.calculateTotalRowHeight()
+ + body.spacerContainer.getSpacerHeightsSum();
+ double scrollContentWidth = columnConfiguration.calculateRowWidth();
+ double tableWrapperHeight = heightOfEscalator;
+ double tableWrapperWidth = widthOfEscalator;
+
+ boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
+ + WidgetUtil.PIXEL_EPSILON
+ - header.getHeightOfSection()
+ - footer.getHeightOfSection();
+ boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
+ + WidgetUtil.PIXEL_EPSILON;
+
+ // One dimension got scrollbars, but not the other. Recheck time!
+ if (verticalScrollNeeded != horizontalScrollNeeded) {
+ if (!verticalScrollNeeded && horizontalScrollNeeded) {
+ verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
+ + WidgetUtil.PIXEL_EPSILON
+ - header.getHeightOfSection()
+ - footer.getHeightOfSection()
+ - horizontalScrollbar.getScrollbarThickness();
+ } else {
+ horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
+ + WidgetUtil.PIXEL_EPSILON
+ - verticalScrollbar.getScrollbarThickness();
+ }
+ }
+
+ // let's fix the table wrapper size, since it's now stable.
+ if (verticalScrollNeeded) {
+ tableWrapperWidth -= verticalScrollbar.getScrollbarThickness();
+ tableWrapperWidth = Math.max(0, tableWrapperWidth);
+ }
+ if (horizontalScrollNeeded) {
+ tableWrapperHeight -= horizontalScrollbar
+ .getScrollbarThickness();
+ tableWrapperHeight = Math.max(0, tableWrapperHeight);
+ }
+ tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX);
+ tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX);
+
+ double footerHeight = footer.getHeightOfSection();
+ double headerHeight = header.getHeightOfSection();
+ double vScrollbarHeight = Math.max(0, tableWrapperHeight
+ - footerHeight - headerHeight);
+ verticalScrollbar.setOffsetSize(vScrollbarHeight);
+ verticalScrollbar.setScrollSize(scrollContentHeight);
+
+ /*
+ * If decreasing the amount of frozen columns, and scrolled to the
+ * right, the scroll position might reset. So we need to remember
+ * the scroll position, and re-apply it once the scrollbar size has
+ * been adjusted.
+ */
+ double prevScrollPos = horizontalScrollbar.getScrollPos();
+
+ double unfrozenPixels = columnConfiguration
+ .getCalculatedColumnsWidth(Range.between(
+ columnConfiguration.getFrozenColumnCount(),
+ columnConfiguration.getColumnCount()));
+ double frozenPixels = scrollContentWidth - unfrozenPixels;
+ double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
+ horizontalScrollbar.setOffsetSize(hScrollOffsetWidth);
+ horizontalScrollbar.setScrollSize(unfrozenPixels);
+ horizontalScrollbar.getElement().getStyle()
+ .setLeft(frozenPixels, Unit.PX);
+ horizontalScrollbar.setScrollPos(prevScrollPos);
+
+ /*
+ * only show the scrollbar wrapper if the scrollbar itself is
+ * visible.
+ */
+ if (horizontalScrollbar.showsScrollHandle()) {
+ horizontalScrollbarDeco.getStyle().clearDisplay();
+ } else {
+ horizontalScrollbarDeco.getStyle().setDisplay(Display.NONE);
+ }
+
+ /*
+ * only show corner background divs if the vertical scrollbar is
+ * visible.
+ */
+ Style hCornerStyle = headerDeco.getStyle();
+ Style fCornerStyle = footerDeco.getStyle();
+ if (verticalScrollbar.showsScrollHandle()) {
+ hCornerStyle.clearDisplay();
+ fCornerStyle.clearDisplay();
+
+ if (horizontalScrollbar.showsScrollHandle()) {
+ double offset = horizontalScrollbar.getScrollbarThickness();
+ fCornerStyle.setBottom(offset, Unit.PX);
+ } else {
+ fCornerStyle.clearBottom();
+ }
+ } else {
+ hCornerStyle.setDisplay(Display.NONE);
+ fCornerStyle.setDisplay(Display.NONE);
+ }
+ }
+
+ /**
+ * Logical scrolling event handler for the entire widget.
+ */
+ public void onScroll() {
+
+ final double scrollTop = verticalScrollbar.getScrollPos();
+ final double scrollLeft = horizontalScrollbar.getScrollPos();
+ if (lastScrollLeft != scrollLeft) {
+ for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
+ header.updateFreezePosition(i, scrollLeft);
+ body.updateFreezePosition(i, scrollLeft);
+ footer.updateFreezePosition(i, scrollLeft);
+ }
+
+ position.set(headElem, -scrollLeft, 0);
+
+ /*
+ * TODO [[optimize]]: cache this value in case the instanceof
+ * check has undesirable overhead. This could also be a
+ * candidate for some deferred binding magic so that e.g.
+ * AbsolutePosition is not even considered in permutations that
+ * we know support something better. That would let the compiler
+ * completely remove the entire condition since it knows that
+ * the if will never be true.
+ */
+ if (position instanceof AbsolutePosition) {
+ /*
+ * we don't want to put "top: 0" on the footer, since it'll
+ * render wrong, as we already have
+ * "bottom: $footer-height".
+ */
+ footElem.getStyle().setLeft(-scrollLeft, Unit.PX);
+ } else {
+ position.set(footElem, -scrollLeft, 0);
+ }
+
+ lastScrollLeft = scrollLeft;
+ }
+
+ body.setBodyScrollPosition(scrollLeft, scrollTop);
+
+ lastScrollTop = scrollTop;
+ body.updateEscalatorRowsOnScroll();
+ body.spacerContainer.updateSpacerDecosVisibility();
+ /*
+ * TODO [[optimize]]: Might avoid a reflow by first calculating new
+ * scrolltop and scrolleft, then doing the escalator magic based on
+ * those numbers and only updating the positions after that.
+ */
+ }
+
+ public native void attachScrollListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.addEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.attachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
+ }
+ }-*/;
+
+ public native void detachScrollListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.removeEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.detachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
+ }
+ }-*/;
+
+ public native void attachMousewheelListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel';
+ element.addEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
+ } else {
+ // IE8
+ element.attachEvent("onmousewheel", this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
+ }
+ }-*/;
+
+ public native void detachMousewheelListener(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = element.onwheel===undefined?"mousewheel":"wheel";
+ element.removeEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
+ } else {
+ // IE8
+ element.detachEvent("onmousewheel", this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
+ }
+ }-*/;
+
+ public native void attachTouchListeners(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.addEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
+ element.addEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
+ element.addEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
+ element.addEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
+ } else {
+ // this would be IE8, but we don't support it with touch
+ }
+ }-*/;
+
+ public native void detachTouchListeners(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.removeEventListener) {
+ element.removeEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
+ element.removeEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
+ element.removeEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
+ element.removeEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
+ } else {
+ // this would be IE8, but we don't support it with touch
+ }
+ }-*/;
+
+ public void scrollToColumn(final int columnIndex,
+ final ScrollDestination destination, final int padding) {
+ assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column";
+
+ /*
+ * To cope with frozen columns, we just pretend those columns are
+ * not there at all when calculating the position of the target
+ * column and the boundaries of the viewport. The resulting
+ * scrollLeft will be correct without compensation since the DOM
+ * structure effectively means that scrollLeft also ignores the
+ * frozen columns.
+ */
+ final double frozenPixels = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0,
+ columnConfiguration.frozenColumns));
+
+ final double targetStartPx = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0, columnIndex))
+ - frozenPixels;
+ final double targetEndPx = targetStartPx
+ + columnConfiguration.getColumnWidthActual(columnIndex);
+
+ final double viewportStartPx = getScrollLeft();
+ double viewportEndPx = viewportStartPx
+ + WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(getElement())
+ - frozenPixels;
+ if (verticalScrollbar.showsScrollHandle()) {
+ viewportEndPx -= WidgetUtil.getNativeScrollbarSize();
+ }
+
+ final double scrollLeft = getScrollPos(destination, targetStartPx,
+ targetEndPx, viewportStartPx, viewportEndPx, padding);
+
+ /*
+ * note that it doesn't matter if the scroll would go beyond the
+ * content, since the browser will adjust for that, and everything
+ * fall into line accordingly.
+ */
+ setScrollLeft(scrollLeft);
+ }
+
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination, final double padding) {
+
+ final double targetStartPx = (body.getDefaultRowHeight() * rowIndex)
+ + body.spacerContainer
+ .getSpacerHeightsSumUntilIndex(rowIndex);
+ final double targetEndPx = targetStartPx
+ + body.getDefaultRowHeight();
+
+ final double viewportStartPx = getScrollTop();
+ final double viewportEndPx = viewportStartPx
+ + body.getHeightOfSection();
+
+ final double scrollTop = getScrollPos(destination, targetStartPx,
+ targetEndPx, viewportStartPx, viewportEndPx, padding);
+
+ /*
+ * note that it doesn't matter if the scroll would go beyond the
+ * content, since the browser will adjust for that, and everything
+ * falls into line accordingly.
+ */
+ setScrollTop(scrollTop);
+ }
+ }
+
+ protected abstract class AbstractRowContainer implements RowContainer {
+ private EscalatorUpdater updater = EscalatorUpdater.NULL;
+
+ private int rows;
+
+ /**
+ * The table section element ({@code <thead>}, {@code <tbody>} or
+ * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in.
+ */
+ protected final TableSectionElement root;
+
+ /**
+ * The primary style name of the escalator. Most commonly provided by
+ * Escalator as "v-escalator".
+ */
+ private String primaryStyleName = null;
+
+ private boolean defaultRowHeightShouldBeAutodetected = true;
+
+ private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
+
+ public AbstractRowContainer(
+ final TableSectionElement rowContainerElement) {
+ root = rowContainerElement;
+ }
+
+ @Override
+ public TableSectionElement getElement() {
+ return root;
+ }
+
+ /**
+ * Gets the tag name of an element to represent a cell in a row.
+ * <p>
+ * Usually {@code "th"} or {@code "td"}.
+ * <p>
+ * <em>Note:</em> To actually <em>create</em> such an element, use
+ * {@link #createCellElement(int, int)} instead.
+ *
+ * @return the tag name for the element to represent cells as
+ * @see #createCellElement(int, int)
+ */
+ protected abstract String getCellElementTagName();
+
+ @Override
+ public EscalatorUpdater getEscalatorUpdater() {
+ return updater;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for rows or columns
+ * when this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void setEscalatorUpdater(final EscalatorUpdater escalatorUpdater) {
+ if (escalatorUpdater == null) {
+ throw new IllegalArgumentException(
+ "escalator updater cannot be null");
+ }
+
+ updater = escalatorUpdater;
+
+ if (hasColumnAndRowData() && getRowCount() > 0) {
+ refreshRows(0, getRowCount());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there are no rows in the DOM when
+ * this method is called.
+ *
+ * @see #hasSomethingInDom()
+ */
+ @Override
+ public void removeRows(final int index, final int numberOfRows) {
+ assertArgumentsAreValidAndWithinRange(index, numberOfRows);
+
+ rows -= numberOfRows;
+
+ if (!isAttached()) {
+ return;
+ }
+
+ if (hasSomethingInDom()) {
+ paintRemoveRows(index, numberOfRows);
+ }
+ }
+
+ /**
+ * Removes those row elements from the DOM that correspond to the given
+ * range of logical indices. This may be fewer than {@code numberOfRows}
+ * , even zero, if not all the removed rows are actually visible.
+ * <p>
+ * The implementation must call {@link #paintRemoveRow(Element, int)}
+ * for each row that is removed from the DOM.
+ *
+ * @param index
+ * the logical index of the first removed row
+ * @param numberOfRows
+ * number of logical rows to remove
+ */
+ protected abstract void paintRemoveRows(final int index,
+ final int numberOfRows);
+
+ /**
+ * Removes a row element from the DOM, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and
+ * {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before
+ * and after removing the row, respectively.
+ * <p>
+ * This method must be called for each removed DOM row by any
+ * {@link #paintRemoveRows(int, int)} implementation.
+ *
+ * @param tr
+ * the row element to remove.
+ */
+ protected void paintRemoveRow(final TableRowElement tr,
+ final int logicalRowIndex) {
+
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ getEscalatorUpdater().preDetach(flyweightRow,
+ flyweightRow.getCells());
+
+ tr.removeFromParent();
+
+ getEscalatorUpdater().postDetach(flyweightRow,
+ flyweightRow.getCells());
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+
+ }
+
+ protected void assertArgumentsAreValidAndWithinRange(final int index,
+ final int numberOfRows) throws IllegalArgumentException,
+ IndexOutOfBoundsException {
+ if (numberOfRows < 1) {
+ throw new IllegalArgumentException(
+ "Number of rows must be 1 or greater (was "
+ + numberOfRows + ")");
+ }
+
+ if (index < 0 || index + numberOfRows > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "row range (" + index + ".." + (index + numberOfRows)
+ + ") was outside of the current number of rows ("
+ + getRowCount() + ")");
+ }
+ }
+
+ @Override
+ public int getRowCount() {
+ return rows;
+ }
+
+ /**
+ * This method calculates the current row count directly from the DOM.
+ * <p>
+ * While Escalator is stable, this value should equal to
+ * {@link #getRowCount()}, but while row counts are being updated, these
+ * two values might differ for a short while.
+ * <p>
+ * Any extra content, such as spacers for the body, should not be
+ * included in this count.
+ *
+ * @since 7.5.0
+ *
+ * @return the actual DOM count of rows
+ */
+ public abstract int getDomRowCount();
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for columns when
+ * this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void insertRows(final int index, final int numberOfRows) {
+ if (index < 0 || index > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given index (" + index
+ + ") was outside of the current number of rows (0.."
+ + getRowCount() + ")");
+ }
+
+ if (numberOfRows < 1) {
+ throw new IllegalArgumentException(
+ "Number of rows must be 1 or greater (was "
+ + numberOfRows + ")");
+ }
+
+ rows += numberOfRows;
+
+ /*
+ * only add items in the DOM if the widget itself is attached to the
+ * DOM. We can't calculate sizes otherwise.
+ */
+ if (isAttached()) {
+ paintInsertRows(index, numberOfRows);
+
+ if (rows == numberOfRows) {
+ /*
+ * We are inserting the first rows in this container. We
+ * potentially need to set the widths for the cells for the
+ * first time.
+ */
+ Map<Integer, Double> colWidths = new HashMap<Integer, Double>();
+ for (int i = 0; i < getColumnConfiguration()
+ .getColumnCount(); i++) {
+ Double width = Double.valueOf(getColumnConfiguration()
+ .getColumnWidth(i));
+ Integer col = Integer.valueOf(i);
+ colWidths.put(col, width);
+ }
+ getColumnConfiguration().setColumnWidths(colWidths);
+ }
+ }
+ }
+
+ /**
+ * Actually add rows into the DOM, now that everything can be
+ * calculated.
+ *
+ * @param visualIndex
+ * the DOM index to add rows into
+ * @param numberOfRows
+ * the number of rows to insert
+ * @return a list of the added row elements
+ */
+ protected abstract void paintInsertRows(final int visualIndex,
+ final int numberOfRows);
+
+ protected List<TableRowElement> paintInsertStaticRows(
+ final int visualIndex, final int numberOfRows) {
+ assert isAttached() : "Can't paint rows if Escalator is not attached";
+
+ final List<TableRowElement> addedRows = new ArrayList<TableRowElement>();
+
+ if (numberOfRows < 1) {
+ return addedRows;
+ }
+
+ Node referenceRow;
+ if (root.getChildCount() != 0 && visualIndex != 0) {
+ // get the row node we're inserting stuff after
+ referenceRow = root.getChild(visualIndex - 1);
+ } else {
+ // index is 0, so just prepend.
+ referenceRow = null;
+ }
+
+ for (int row = visualIndex; row < visualIndex + numberOfRows; row++) {
+ final TableRowElement tr = TableRowElement.as(DOM.createTR());
+ addedRows.add(tr);
+ tr.addClassName(getStylePrimaryName() + "-row");
+
+ for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
+ final double colWidth = columnConfiguration
+ .getColumnWidthActual(col);
+ final TableCellElement cellElem = createCellElement(colWidth);
+ tr.appendChild(cellElem);
+
+ // Set stylename and position if new cell is frozen
+ if (col < columnConfiguration.frozenColumns) {
+ cellElem.addClassName("frozen");
+ position.set(cellElem, scroller.lastScrollLeft, 0);
+ }
+ if (columnConfiguration.frozenColumns > 0
+ && col == columnConfiguration.frozenColumns - 1) {
+ cellElem.addClassName("last-frozen");
+ }
+ }
+
+ referenceRow = paintInsertRow(referenceRow, tr, row);
+ }
+ reapplyRowWidths();
+
+ recalculateSectionHeight();
+
+ return addedRows;
+ }
+
+ /**
+ * Inserts a single row into the DOM, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
+ * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
+ * and after inserting the row, respectively. The row should have its
+ * cells already inserted.
+ *
+ * @param referenceRow
+ * the row after which to insert or null if insert as first
+ * @param tr
+ * the row to be inserted
+ * @param logicalRowIndex
+ * the logical index of the inserted row
+ * @return the inserted row to be used as the new reference
+ */
+ protected Node paintInsertRow(Node referenceRow,
+ final TableRowElement tr, int logicalRowIndex) {
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ getEscalatorUpdater().preAttach(flyweightRow,
+ flyweightRow.getCells());
+
+ referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
+ referenceRow);
+
+ getEscalatorUpdater().postAttach(flyweightRow,
+ flyweightRow.getCells());
+ updater.update(flyweightRow, flyweightRow.getCells());
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+ return referenceRow;
+ }
+
+ private Node insertAfterReferenceAndUpdateIt(final Element parent,
+ final Element elem, final Node referenceNode) {
+ if (referenceNode != null) {
+ parent.insertAfter(elem, referenceNode);
+ } else {
+ /*
+ * referencenode being null means we have offset 0, i.e. make it
+ * the first row
+ */
+ /*
+ * TODO [[optimize]]: Is insertFirst or append faster for an
+ * empty root?
+ */
+ parent.insertFirst(elem);
+ }
+ return elem;
+ }
+
+ abstract protected void recalculateSectionHeight();
+
+ /**
+ * Returns the height of all rows in the row container.
+ */
+ protected double calculateTotalRowHeight() {
+ return getDefaultRowHeight() * getRowCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for columns when
+ * this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ // overridden because of JavaDoc
+ public void refreshRows(final int index, final int numberOfRows) {
+ Range rowRange = Range.withLength(index, numberOfRows);
+ Range colRange = Range.withLength(0, getColumnConfiguration()
+ .getColumnCount());
+ refreshCells(rowRange, colRange);
+ }
+
+ protected abstract void refreshCells(Range logicalRowRange,
+ Range colRange);
+
+ void refreshRow(TableRowElement tr, int logicalRowIndex) {
+ refreshRow(tr, logicalRowIndex, Range.withLength(0,
+ getColumnConfiguration().getColumnCount()));
+ }
+
+ void refreshRow(final TableRowElement tr, final int logicalRowIndex,
+ Range colRange) {
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+ Iterable<FlyweightCell> cellsToUpdate = flyweightRow.getCells(
+ colRange.getStart(), colRange.length());
+ updater.update(flyweightRow, cellsToUpdate);
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+ }
+
+ /**
+ * Create and setup an empty cell element.
+ *
+ * @param width
+ * the width of the cell, in pixels
+ *
+ * @return a set-up empty cell element
+ */
+ public TableCellElement createCellElement(final double width) {
+ final TableCellElement cellElem = TableCellElement.as(DOM
+ .createElement(getCellElementTagName()));
+
+ final double height = getDefaultRowHeight();
+ assert height >= 0 : "defaultRowHeight was negative. There's a setter leak somewhere.";
+ cellElem.getStyle().setHeight(height, Unit.PX);
+
+ if (width >= 0) {
+ cellElem.getStyle().setWidth(width, Unit.PX);
+ }
+ cellElem.addClassName(getStylePrimaryName() + "-cell");
+ return cellElem;
+ }
+
+ @Override
+ public TableRowElement getRowElement(int index) {
+ return getTrByVisualIndex(index);
+ }
+
+ /**
+ * Gets the child element that is visually at a certain index
+ *
+ * @param index
+ * the index of the element to retrieve
+ * @return the element at position {@code index}
+ * @throws IndexOutOfBoundsException
+ * if {@code index} is not valid within {@link #root}
+ */
+ protected abstract TableRowElement getTrByVisualIndex(int index)
+ throws IndexOutOfBoundsException;
+
+ protected void paintRemoveColumns(final int offset,
+ final int numberOfColumns) {
+ for (int i = 0; i < getDomRowCount(); i++) {
+ TableRowElement row = getTrByVisualIndex(i);
+ flyweightRow.setup(row, i,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ Iterable<FlyweightCell> attachedCells = flyweightRow.getCells(
+ offset, numberOfColumns);
+ getEscalatorUpdater().preDetach(flyweightRow, attachedCells);
+
+ for (int j = 0; j < numberOfColumns; j++) {
+ row.getCells().getItem(offset).removeFromParent();
+ }
+
+ Iterable<FlyweightCell> detachedCells = flyweightRow
+ .getUnattachedCells(offset, numberOfColumns);
+ getEscalatorUpdater().postDetach(flyweightRow, detachedCells);
+
+ assert flyweightRow.teardown();
+ }
+ }
+
+ protected void paintInsertColumns(final int offset,
+ final int numberOfColumns, boolean frozen) {
+
+ for (int row = 0; row < getDomRowCount(); row++) {
+ final TableRowElement tr = getTrByVisualIndex(row);
+ int logicalRowIndex = getLogicalRowIndex(tr);
+ paintInsertCells(tr, logicalRowIndex, offset, numberOfColumns);
+ }
+ reapplyRowWidths();
+
+ if (frozen) {
+ for (int col = offset; col < offset + numberOfColumns; col++) {
+ setColumnFrozen(col, true);
+ }
+ }
+ }
+
+ /**
+ * Inserts new cell elements into a single row element, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
+ * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
+ * and after inserting the cells, respectively.
+ * <p>
+ * Precondition: The row must be already attached to the DOM and the
+ * FlyweightCell instances corresponding to the new columns added to
+ * {@code flyweightRow}.
+ *
+ * @param tr
+ * the row in which to insert the cells
+ * @param logicalRowIndex
+ * the index of the row
+ * @param offset
+ * the index of the first cell
+ * @param numberOfCells
+ * the number of cells to insert
+ */
+ private void paintInsertCells(final TableRowElement tr,
+ int logicalRowIndex, final int offset, final int numberOfCells) {
+
+ assert root.isOrHasChild(tr) : "The row must be attached to the document";
+
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ Iterable<FlyweightCell> cells = flyweightRow.getUnattachedCells(
+ offset, numberOfCells);
+
+ for (FlyweightCell cell : cells) {
+ final double colWidth = columnConfiguration
+ .getColumnWidthActual(cell.getColumn());
+ final TableCellElement cellElem = createCellElement(colWidth);
+ cell.setElement(cellElem);
+ }
+
+ getEscalatorUpdater().preAttach(flyweightRow, cells);
+
+ Node referenceCell;
+ if (offset != 0) {
+ referenceCell = tr.getChild(offset - 1);
+ } else {
+ referenceCell = null;
+ }
+
+ for (FlyweightCell cell : cells) {
+ referenceCell = insertAfterReferenceAndUpdateIt(tr,
+ cell.getElement(), referenceCell);
+ }
+
+ getEscalatorUpdater().postAttach(flyweightRow, cells);
+ getEscalatorUpdater().update(flyweightRow, cells);
+
+ assert flyweightRow.teardown();
+ }
+
+ public void setColumnFrozen(int column, boolean frozen) {
+ toggleFrozenColumnClass(column, frozen, "frozen");
+
+ if (frozen) {
+ updateFreezePosition(column, scroller.lastScrollLeft);
+ }
+ }
+
+ private void toggleFrozenColumnClass(int column, boolean frozen,
+ String className) {
+ final NodeList<TableRowElement> childRows = root.getRows();
+
+ for (int row = 0; row < childRows.getLength(); row++) {
+ final TableRowElement tr = childRows.getItem(row);
+ if (!rowCanBeFrozen(tr)) {
+ continue;
+ }
+
+ TableCellElement cell = tr.getCells().getItem(column);
+ if (frozen) {
+ cell.addClassName(className);
+ } else {
+ cell.removeClassName(className);
+ position.reset(cell);
+ }
+ }
+ }
+
+ public void setColumnLastFrozen(int column, boolean lastFrozen) {
+ toggleFrozenColumnClass(column, lastFrozen, "last-frozen");
+ }
+
+ public void updateFreezePosition(int column, double scrollLeft) {
+ final NodeList<TableRowElement> childRows = root.getRows();
+
+ for (int row = 0; row < childRows.getLength(); row++) {
+ final TableRowElement tr = childRows.getItem(row);
+
+ if (rowCanBeFrozen(tr)) {
+ TableCellElement cell = tr.getCells().getItem(column);
+ position.set(cell, scrollLeft, 0);
+ }
+ }
+ }
+
+ /**
+ * Checks whether a row is an element, or contains such elements, that
+ * can be frozen.
+ * <p>
+ * In practice, this applies for all header and footer rows. For body
+ * rows, it applies for all rows except spacer rows.
+ *
+ * @since 7.5.0
+ *
+ * @param tr
+ * the row element to check for if it is or has elements that
+ * can be frozen
+ * @return <code>true</code> iff this the given element, or any of its
+ * descendants, can be frozen
+ */
+ abstract protected boolean rowCanBeFrozen(TableRowElement tr);
+
+ /**
+ * Iterates through all the cells in a column and returns the width of
+ * the widest element in this RowContainer.
+ *
+ * @param index
+ * the index of the column to inspect
+ * @return the pixel width of the widest element in the indicated column
+ */
+ public double calculateMaxColWidth(int index) {
+ TableRowElement row = TableRowElement.as(root
+ .getFirstChildElement());
+ double maxWidth = 0;
+ while (row != null) {
+ final TableCellElement cell = row.getCells().getItem(index);
+ final boolean isVisible = !cell.getStyle().getDisplay()
+ .equals(Display.NONE.getCssName());
+ if (isVisible) {
+ maxWidth = Math.max(maxWidth, WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(cell));
+ }
+ row = TableRowElement.as(row.getNextSiblingElement());
+ }
+ return maxWidth;
+ }
+
+ /**
+ * Reapplies all the cells' widths according to the calculated widths in
+ * the column configuration.
+ */
+ public void reapplyColumnWidths() {
+ Element row = root.getFirstChildElement();
+ while (row != null) {
+ // Only handle non-spacer rows
+ if (!body.spacerContainer.isSpacer(row)) {
+ Element cell = row.getFirstChildElement();
+ int columnIndex = 0;
+ while (cell != null) {
+ final double width = getCalculatedColumnWidthWithColspan(
+ cell, columnIndex);
+
+ /*
+ * TODO Should Escalator implement ProvidesResize at
+ * some point, this is where we need to do that.
+ */
+ cell.getStyle().setWidth(width, Unit.PX);
+
+ cell = cell.getNextSiblingElement();
+ columnIndex++;
+ }
+ }
+ row = row.getNextSiblingElement();
+ }
+
+ reapplyRowWidths();
+ }
+
+ private double getCalculatedColumnWidthWithColspan(final Element cell,
+ final int columnIndex) {
+ final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
+ Range spannedColumns = Range.withLength(columnIndex, colspan);
+
+ /*
+ * Since browsers don't explode with overflowing colspans, escalator
+ * shouldn't either.
+ */
+ if (spannedColumns.getEnd() > columnConfiguration.getColumnCount()) {
+ spannedColumns = Range.between(columnIndex,
+ columnConfiguration.getColumnCount());
+ }
+ return columnConfiguration
+ .getCalculatedColumnsWidth(spannedColumns);
+ }
+
+ /**
+ * Applies the total length of the columns to each row element.
+ * <p>
+ * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this
+ * method only modifies the width of the {@code <tr>} element, not the
+ * cells within.
+ */
+ protected void reapplyRowWidths() {
+ double rowWidth = columnConfiguration.calculateRowWidth();
+ if (rowWidth < 0) {
+ return;
+ }
+
+ Element row = root.getFirstChildElement();
+ while (row != null) {
+ // IF there is a rounding error when summing the columns, we
+ // need to round the tr width up to ensure that columns fit and
+ // do not wrap
+ // E.g.122.95+123.25+103.75+209.25+83.52+88.57+263.45+131.21+126.85+113.13=1365.9299999999998
+ // For this we must set 1365.93 or the last column will wrap
+ row.getStyle().setWidth(WidgetUtil.roundSizeUp(rowWidth),
+ Unit.PX);
+ row = row.getNextSiblingElement();
+ }
+ }
+
+ /**
+ * The primary style name for the container.
+ *
+ * @param primaryStyleName
+ * the style name to use as prefix for all row and cell style
+ * names.
+ */
+ protected void setStylePrimaryName(String primaryStyleName) {
+ String oldStyle = getStylePrimaryName();
+ if (SharedUtil.equals(oldStyle, primaryStyleName)) {
+ return;
+ }
+
+ this.primaryStyleName = primaryStyleName;
+
+ // Update already rendered rows and cells
+ Element row = root.getRows().getItem(0);
+ while (row != null) {
+ UIObject.setStylePrimaryName(row, primaryStyleName + "-row");
+ Element cell = TableRowElement.as(row).getCells().getItem(0);
+ while (cell != null) {
+ assert TableCellElement.is(cell);
+ UIObject.setStylePrimaryName(cell, primaryStyleName
+ + "-cell");
+ cell = cell.getNextSiblingElement();
+ }
+ row = row.getNextSiblingElement();
+ }
+ }
+
+ /**
+ * Returns the primary style name of the container.
+ *
+ * @return The primary style name or <code>null</code> if not set.
+ */
+ protected String getStylePrimaryName() {
+ return primaryStyleName;
+ }
+
+ @Override
+ public void setDefaultRowHeight(double px)
+ throws IllegalArgumentException {
+ if (px < 1) {
+ throw new IllegalArgumentException("Height must be positive. "
+ + px + " was given.");
+ }
+
+ defaultRowHeightShouldBeAutodetected = false;
+ defaultRowHeight = px;
+ reapplyDefaultRowHeights();
+ }
+
+ @Override
+ public double getDefaultRowHeight() {
+ return defaultRowHeight;
+ }
+
+ /**
+ * The default height of rows has (most probably) changed.
+ * <p>
+ * Make sure that the displayed rows with a default height are updated
+ * in height and top position.
+ * <p>
+ * <em>Note:</em>This implementation should not call
+ * {@link Escalator#recalculateElementSizes()} - it is done by the
+ * discretion of the caller of this method.
+ */
+ protected abstract void reapplyDefaultRowHeights();
+
+ protected void reapplyRowHeight(final TableRowElement tr,
+ final double heightPx) {
+ assert heightPx >= 0 : "Height must not be negative";
+
+ Element cellElem = tr.getFirstChildElement();
+ while (cellElem != null) {
+ cellElem.getStyle().setHeight(heightPx, Unit.PX);
+ cellElem = cellElem.getNextSiblingElement();
+ }
+
+ /*
+ * no need to apply height to tr-element, it'll be resized
+ * implicitly.
+ */
+ }
+
+ protected void setRowPosition(final TableRowElement tr, final int x,
+ final double y) {
+ positions.set(tr, x, y);
+ }
+
+ /**
+ * Returns <em>the assigned</em> top position for the given element.
+ * <p>
+ * <em>Note:</em> This method does not calculate what a row's top
+ * position should be. It just returns an assigned value, correct or
+ * not.
+ *
+ * @param tr
+ * the table row element to measure
+ * @return the current top position for {@code tr}
+ * @see BodyRowContainerImpl#getRowTop(int)
+ */
+ protected double getRowTop(final TableRowElement tr) {
+ return positions.getTop(tr);
+ }
+
+ protected void removeRowPosition(TableRowElement tr) {
+ positions.remove(tr);
+ }
+
+ public void autodetectRowHeightLater() {
+ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (defaultRowHeightShouldBeAutodetected && isAttached()) {
+ autodetectRowHeightNow();
+ defaultRowHeightShouldBeAutodetected = false;
+ }
+ }
+ });
+ }
+
+ public void autodetectRowHeightNow() {
+ if (!isAttached()) {
+ // Run again when attached
+ defaultRowHeightShouldBeAutodetected = true;
+ return;
+ }
+
+ final Element detectionTr = DOM.createTR();
+ detectionTr.setClassName(getStylePrimaryName() + "-row");
+
+ final Element cellElem = DOM.createElement(getCellElementTagName());
+ cellElem.setClassName(getStylePrimaryName() + "-cell");
+ cellElem.setInnerText("Ij");
+
+ detectionTr.appendChild(cellElem);
+ root.appendChild(detectionTr);
+ double boundingHeight = WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(cellElem);
+ defaultRowHeight = Math.max(1.0d, boundingHeight);
+ root.removeChild(detectionTr);
+
+ if (root.hasChildNodes()) {
+ reapplyDefaultRowHeights();
+ applyHeightByRows();
+ }
+ }
+
+ @Override
+ public Cell getCell(final Element element) {
+ if (element == null) {
+ throw new IllegalArgumentException("Element cannot be null");
+ }
+
+ /*
+ * Ensure that element is not root nor the direct descendant of root
+ * (a row) and ensure the element is inside the dom hierarchy of the
+ * root element. If not, return.
+ */
+ if (root == element || element.getParentElement() == root
+ || !root.isOrHasChild(element)) {
+ return null;
+ }
+
+ /*
+ * Ensure element is the cell element by iterating up the DOM
+ * hierarchy until reaching cell element.
+ */
+ Element cellElementCandidate = element;
+ while (cellElementCandidate.getParentElement().getParentElement() != root) {
+ cellElementCandidate = cellElementCandidate.getParentElement();
+ }
+ final TableCellElement cellElement = TableCellElement
+ .as(cellElementCandidate);
+
+ // Find dom column
+ int domColumnIndex = -1;
+ for (Element e = cellElement; e != null; e = e
+ .getPreviousSiblingElement()) {
+ domColumnIndex++;
+ }
+
+ // Find dom row
+ int domRowIndex = -1;
+ for (Element e = cellElement.getParentElement(); e != null; e = e
+ .getPreviousSiblingElement()) {
+ domRowIndex++;
+ }
+
+ return new Cell(domRowIndex, domColumnIndex, cellElement);
+ }
+
+ double measureCellWidth(TableCellElement cell, boolean withContent) {
+ /*
+ * To get the actual width of the contents, we need to get the cell
+ * content without any hardcoded height or width.
+ *
+ * But we don't want to modify the existing column, because that
+ * might trigger some unnecessary listeners and whatnot. So,
+ * instead, we make a deep clone of that cell, but without any
+ * explicit dimensions, and measure that instead.
+ */
+
+ TableCellElement cellClone = TableCellElement.as((Element) cell
+ .cloneNode(withContent));
+ cellClone.getStyle().clearHeight();
+ cellClone.getStyle().clearWidth();
+
+ cell.getParentElement().insertBefore(cellClone, cell);
+ double requiredWidth = WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(cellClone);
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * IE browsers have some issues with subpixels. Occasionally
+ * content is overflown even if not necessary. Increase the
+ * counted required size by 0.01 just to be on the safe side.
+ */
+ requiredWidth += 0.01;
+ }
+
+ cellClone.removeFromParent();
+
+ return requiredWidth;
+ }
+
+ /**
+ * Gets the minimum width needed to display the cell properly.
+ *
+ * @param colIndex
+ * index of column to measure
+ * @param withContent
+ * <code>true</code> if content is taken into account,
+ * <code>false</code> if not
+ * @return cell width needed for displaying correctly
+ */
+ double measureMinCellWidth(int colIndex, boolean withContent) {
+ assert isAttached() : "Can't measure max width of cell, since Escalator is not attached to the DOM.";
+
+ double minCellWidth = -1;
+ NodeList<TableRowElement> rows = root.getRows();
+
+ for (int row = 0; row < rows.getLength(); row++) {
+
+ TableCellElement cell = rows.getItem(row).getCells()
+ .getItem(colIndex);
+
+ if (cell != null && !cellIsPartOfSpan(cell)) {
+ double cellWidth = measureCellWidth(cell, withContent);
+ minCellWidth = Math.max(minCellWidth, cellWidth);
+ }
+ }
+
+ return minCellWidth;
+ }
+
+ private boolean cellIsPartOfSpan(TableCellElement cell) {
+ boolean cellHasColspan = cell.getColSpan() > 1;
+ boolean cellIsHidden = Display.NONE.getCssName().equals(
+ cell.getStyle().getDisplay());
+ return cellHasColspan || cellIsHidden;
+ }
+
+ void refreshColumns(int index, int numberOfColumns) {
+ if (getRowCount() > 0) {
+ Range rowRange = Range.withLength(0, getRowCount());
+ Range colRange = Range.withLength(index, numberOfColumns);
+ refreshCells(rowRange, colRange);
+ }
+ }
+
+ /**
+ * The height of this table section.
+ * <p>
+ * Note that {@link Escalator#getBody() the body} will calculate its
+ * height, while the others will return a precomputed value.
+ *
+ * @since 7.5.0
+ *
+ * @return the height of this table section
+ */
+ protected abstract double getHeightOfSection();
+
+ protected int getLogicalRowIndex(final TableRowElement tr) {
+ return tr.getSectionRowIndex();
+ };
+
+ }
+
+ private abstract class AbstractStaticRowContainer extends
+ AbstractRowContainer {
+
+ /** The height of the combined rows in the DOM. Never negative. */
+ private double heightOfSection = 0;
+
+ public AbstractStaticRowContainer(final TableSectionElement headElement) {
+ super(headElement);
+ }
+
+ @Override
+ public int getDomRowCount() {
+ return root.getChildCount();
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+ for (int i = index; i < index + numberOfRows; i++) {
+ final TableRowElement tr = root.getRows().getItem(index);
+ paintRemoveRow(tr, index);
+ }
+ recalculateSectionHeight();
+ }
+
+ @Override
+ protected TableRowElement getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < root.getChildCount()) {
+ return root.getRows().getItem(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+
+ @Override
+ public void insertRows(int index, int numberOfRows) {
+ super.insertRows(index, numberOfRows);
+ recalculateElementSizes();
+ applyHeightByRows();
+ }
+
+ @Override
+ public void removeRows(int index, int numberOfRows) {
+
+ /*
+ * While the rows in a static section are removed, the scrollbar is
+ * temporarily shrunk and then re-expanded. This leads to the fact
+ * that the scroll position is scooted up a bit. This means that we
+ * need to reset the position here.
+ *
+ * If Escalator, at some point, gets a JIT evaluation functionality,
+ * this re-setting is a strong candidate for removal.
+ */
+ double oldScrollPos = verticalScrollbar.getScrollPos();
+
+ super.removeRows(index, numberOfRows);
+ recalculateElementSizes();
+ applyHeightByRows();
+
+ verticalScrollbar.setScrollPos(oldScrollPos);
+ }
+
+ @Override
+ protected void reapplyDefaultRowHeights() {
+ if (root.getChildCount() == 0) {
+ return;
+ }
+
+ Profiler.enter("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
+
+ Element tr = root.getRows().getItem(0);
+ while (tr != null) {
+ reapplyRowHeight(TableRowElement.as(tr), getDefaultRowHeight());
+ tr = tr.getNextSiblingElement();
+ }
+
+ /*
+ * Because all rows are immediately displayed in the static row
+ * containers, the section's overall height has most probably
+ * changed.
+ */
+ recalculateSectionHeight();
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
+ }
+
+ @Override
+ protected void recalculateSectionHeight() {
+ Profiler.enter("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
+
+ double newHeight = calculateTotalRowHeight();
+ if (newHeight != heightOfSection) {
+ heightOfSection = newHeight;
+ sectionHeightCalculated();
+
+ /*
+ * We need to update the scrollbar dimension at this point. If
+ * we are scrolled too far down and the static section shrinks,
+ * the body will try to render rows that don't exist during
+ * body.verifyEscalatorCount. This is because the logical row
+ * indices are calculated from the scrollbar position.
+ */
+ verticalScrollbar.setOffsetSize(heightOfEscalator
+ - header.getHeightOfSection()
+ - footer.getHeightOfSection());
+
+ body.verifyEscalatorCount();
+ body.spacerContainer.updateSpacerDecosVisibility();
+ }
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
+ }
+
+ /**
+ * Informs the row container that the height of its respective table
+ * section has changed.
+ * <p>
+ * These calculations might affect some layouting logic, such as the
+ * body is being offset by the footer, the footer needs to be readjusted
+ * according to its height, and so on.
+ * <p>
+ * A table section is either header, body or footer.
+ */
+ protected abstract void sectionHeightCalculated();
+
+ @Override
+ protected void refreshCells(Range logicalRowRange, Range colRange) {
+ Profiler.enter("Escalator.AbstractStaticRowContainer.refreshRows");
+
+ assertArgumentsAreValidAndWithinRange(logicalRowRange.getStart(),
+ logicalRowRange.length());
+
+ if (!isAttached()) {
+ return;
+ }
+
+ if (hasColumnAndRowData()) {
+ for (int row = logicalRowRange.getStart(); row < logicalRowRange
+ .getEnd(); row++) {
+ final TableRowElement tr = getTrByVisualIndex(row);
+ refreshRow(tr, row, colRange);
+ }
+ }
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.refreshRows");
+ }
+
+ @Override
+ protected void paintInsertRows(int visualIndex, int numberOfRows) {
+ paintInsertStaticRows(visualIndex, numberOfRows);
+ }
+
+ @Override
+ protected boolean rowCanBeFrozen(TableRowElement tr) {
+ assert root.isOrHasChild(tr) : "Row does not belong to this table section";
+ return true;
+ }
+
+ @Override
+ protected double getHeightOfSection() {
+ return Math.max(0, heightOfSection);
+ }
+ }
+
+ private class HeaderRowContainer extends AbstractStaticRowContainer {
+ public HeaderRowContainer(final TableSectionElement headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void sectionHeightCalculated() {
+ double heightOfSection = getHeightOfSection();
+ bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
+ spacerDecoContainer.getStyle().setMarginTop(heightOfSection,
+ Unit.PX);
+ verticalScrollbar.getElement().getStyle()
+ .setTop(heightOfSection, Unit.PX);
+ headerDeco.getStyle().setHeight(heightOfSection, Unit.PX);
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "th";
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
+ }
+ }
+
+ private class FooterRowContainer extends AbstractStaticRowContainer {
+ public FooterRowContainer(final TableSectionElement footElement) {
+ super(footElement);
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "td";
+ }
+
+ @Override
+ protected void sectionHeightCalculated() {
+ double headerHeight = header.getHeightOfSection();
+ double footerHeight = footer.getHeightOfSection();
+ int vscrollHeight = (int) Math.floor(heightOfEscalator
+ - headerHeight - footerHeight);
+
+ final boolean horizontalScrollbarNeeded = columnConfiguration
+ .calculateRowWidth() > widthOfEscalator;
+ if (horizontalScrollbarNeeded) {
+ vscrollHeight -= horizontalScrollbar.getScrollbarThickness();
+ }
+
+ footerDeco.getStyle().setHeight(footer.getHeightOfSection(),
+ Unit.PX);
+
+ verticalScrollbar.setOffsetSize(vscrollHeight);
+ }
+ }
+
+ private class BodyRowContainerImpl extends AbstractRowContainer implements
+ BodyRowContainer {
+ /*
+ * TODO [[optimize]]: check whether a native JsArray might be faster
+ * than LinkedList
+ */
+ /**
+ * The order in which row elements are rendered visually in the browser,
+ * with the help of CSS tricks. Usually has nothing to do with the DOM
+ * order.
+ *
+ * @see #sortDomElements()
+ */
+ private final LinkedList<TableRowElement> visualRowOrder = new LinkedList<TableRowElement>();
+
+ /**
+ * The logical index of the topmost row.
+ *
+ * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)},
+ * {@link #updateTopRowLogicalIndex(int)} and
+ * {@link #getTopRowLogicalIndex()} instead
+ */
+ @Deprecated
+ private int topRowLogicalIndex = 0;
+
+ private void setTopRowLogicalIndex(int topRowLogicalIndex) {
+ if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
+ Logger.getLogger("Escalator.BodyRowContainer").fine(
+ "topRowLogicalIndex: " + this.topRowLogicalIndex
+ + " -> " + topRowLogicalIndex);
+ }
+ assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative (top left cell contents: "
+ + visualRowOrder.getFirst().getCells().getItem(0)
+ .getInnerText() + ") ";
+ /*
+ * if there's a smart way of evaluating and asserting the max index,
+ * this would be a nice place to put it. I haven't found out an
+ * effective and generic solution.
+ */
+
+ this.topRowLogicalIndex = topRowLogicalIndex;
+ }
+
+ public int getTopRowLogicalIndex() {
+ return topRowLogicalIndex;
+ }
+
+ private void updateTopRowLogicalIndex(int diff) {
+ setTopRowLogicalIndex(topRowLogicalIndex + diff);
+ }
+
+ private class DeferredDomSorter {
+ private static final int SORT_DELAY_MILLIS = 50;
+
+ // as it happens, 3 frames = 50ms @ 60fps.
+ private static final int REQUIRED_FRAMES_PASSED = 3;
+
+ private final AnimationCallback frameCounter = new AnimationCallback() {
+ @Override
+ public void execute(double timestamp) {
+ framesPassed++;
+ boolean domWasSorted = sortIfConditionsMet();
+ if (!domWasSorted) {
+ animationHandle = AnimationScheduler.get()
+ .requestAnimationFrame(this);
+ } else {
+ waiting = false;
+ }
+ }
+ };
+
+ private int framesPassed;
+ private double startTime;
+ private AnimationHandle animationHandle;
+
+ /** <code>true</code> if a sort is scheduled */
+ public boolean waiting = false;
+
+ public void reschedule() {
+ waiting = true;
+ resetConditions();
+ animationHandle = AnimationScheduler.get()
+ .requestAnimationFrame(frameCounter);
+ }
+
+ private boolean sortIfConditionsMet() {
+ boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED;
+ boolean enoughTimeHasPassed = (Duration.currentTimeMillis() - startTime) >= SORT_DELAY_MILLIS;
+ boolean notTouchActivity = !scroller.touchHandlerBundle.touching;
+ boolean conditionsMet = enoughFramesHavePassed
+ && enoughTimeHasPassed && notTouchActivity;
+
+ if (conditionsMet) {
+ resetConditions();
+ sortDomElements();
+ }
+
+ return conditionsMet;
+ }
+
+ private void resetConditions() {
+ if (animationHandle != null) {
+ animationHandle.cancel();
+ animationHandle = null;
+ }
+ startTime = Duration.currentTimeMillis();
+ framesPassed = 0;
+ }
+ }
+
+ private DeferredDomSorter domSorter = new DeferredDomSorter();
+
+ private final SpacerContainer spacerContainer = new SpacerContainer();
+
+ public BodyRowContainerImpl(final TableSectionElement bodyElement) {
+ super(bodyElement);
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
+ spacerContainer.setStylePrimaryName(primaryStyleName);
+ }
+
+ public void updateEscalatorRowsOnScroll() {
+ if (visualRowOrder.isEmpty()) {
+ return;
+ }
+
+ boolean rowsWereMoved = false;
+
+ final double topElementPosition;
+ final double nextRowBottomOffset;
+ SpacerContainer.SpacerImpl topSpacer = spacerContainer
+ .getSpacer(getTopRowLogicalIndex() - 1);
+
+ if (topSpacer != null) {
+ topElementPosition = topSpacer.getTop();
+ nextRowBottomOffset = topSpacer.getHeight()
+ + getDefaultRowHeight();
+ } else {
+ topElementPosition = getRowTop(visualRowOrder.getFirst());
+ nextRowBottomOffset = getDefaultRowHeight();
+ }
+
+ // TODO [[mpixscroll]]
+ final double scrollTop = tBodyScrollTop;
+ final double viewportOffset = topElementPosition - scrollTop;
+
+ /*
+ * TODO [[optimize]] this if-else can most probably be refactored
+ * into a neater block of code
+ */
+
+ if (viewportOffset > 0) {
+ // there's empty room on top
+
+ double rowPx = getRowHeightsSumBetweenPx(scrollTop,
+ topElementPosition);
+ int originalRowsToMove = (int) Math.ceil(rowPx
+ / getDefaultRowHeight());
+ int rowsToMove = Math.min(originalRowsToMove,
+ visualRowOrder.size());
+
+ final int end = visualRowOrder.size();
+ final int start = end - rowsToMove;
+ final int logicalRowIndex = getLogicalRowIndex(scrollTop);
+
+ moveAndUpdateEscalatorRows(Range.between(start, end), 0,
+ logicalRowIndex);
+
+ setTopRowLogicalIndex(logicalRowIndex);
+
+ rowsWereMoved = true;
+ }
+
+ else if (viewportOffset + nextRowBottomOffset <= 0) {
+ /*
+ * the viewport has been scrolled more than the topmost visual
+ * row.
+ */
+
+ double rowPx = getRowHeightsSumBetweenPx(topElementPosition,
+ scrollTop);
+
+ int originalRowsToMove = (int) (rowPx / getDefaultRowHeight());
+ int rowsToMove = Math.min(originalRowsToMove,
+ visualRowOrder.size());
+
+ int logicalRowIndex;
+ if (rowsToMove < visualRowOrder.size()) {
+ /*
+ * We scroll so little that we can just keep adding the rows
+ * below the current escalator
+ */
+ logicalRowIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ } else {
+ /*
+ * Since we're moving all escalator rows, we need to
+ * calculate the first logical row index from the scroll
+ * position.
+ */
+ logicalRowIndex = getLogicalRowIndex(scrollTop);
+ }
+
+ /*
+ * Since we're moving the viewport downwards, the visual index
+ * is always at the bottom. Note: Due to how
+ * moveAndUpdateEscalatorRows works, this will work out even if
+ * we move all the rows, and try to place them "at the end".
+ */
+ final int targetVisualIndex = visualRowOrder.size();
+
+ // make sure that we don't move rows over the data boundary
+ boolean aRowWasLeftBehind = false;
+ if (logicalRowIndex + rowsToMove > getRowCount()) {
+ /*
+ * TODO [[spacer]]: with constant row heights, there's
+ * always exactly one row that will be moved beyond the data
+ * source, when viewport is scrolled to the end. This,
+ * however, isn't guaranteed anymore once row heights start
+ * varying.
+ */
+ rowsToMove--;
+ aRowWasLeftBehind = true;
+ }
+
+ /*
+ * Make sure we don't scroll beyond the row content. This can
+ * happen if we have spacers for the last rows.
+ */
+ rowsToMove = Math.max(0,
+ Math.min(rowsToMove, getRowCount() - logicalRowIndex));
+
+ moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
+ targetVisualIndex, logicalRowIndex);
+
+ if (aRowWasLeftBehind) {
+ /*
+ * To keep visualRowOrder as a spatially contiguous block of
+ * rows, let's make sure that the one row we didn't move
+ * visually still stays with the pack.
+ */
+ final Range strayRow = Range.withOnly(0);
+
+ /*
+ * We cannot trust getLogicalRowIndex, because it hasn't yet
+ * been updated. But since we're leaving rows behind, it
+ * means we've scrolled to the bottom. So, instead, we
+ * simply count backwards from the end.
+ */
+ final int topLogicalIndex = getRowCount()
+ - visualRowOrder.size();
+ moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
+ }
+
+ final int naiveNewLogicalIndex = getTopRowLogicalIndex()
+ + originalRowsToMove;
+ final int maxLogicalIndex = getRowCount()
+ - visualRowOrder.size();
+ setTopRowLogicalIndex(Math.min(naiveNewLogicalIndex,
+ maxLogicalIndex));
+
+ rowsWereMoved = true;
+ }
+
+ if (rowsWereMoved) {
+ fireRowVisibilityChangeEvent();
+ domSorter.reschedule();
+ }
+ }
+
+ private double getRowHeightsSumBetweenPx(double y1, double y2) {
+ assert y1 < y2 : "y1 must be smaller than y2";
+
+ double viewportPx = y2 - y1;
+ double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1,
+ SpacerInclusionStrategy.PARTIAL, y2,
+ SpacerInclusionStrategy.PARTIAL);
+
+ return viewportPx - spacerPx;
+ }
+
+ private int getLogicalRowIndex(final double px) {
+ double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px);
+ return (int) (rowPx / getDefaultRowHeight());
+ }
+
+ @Override
+ protected void paintInsertRows(final int index, final int numberOfRows) {
+ if (numberOfRows == 0) {
+ return;
+ }
+
+ spacerContainer.shiftSpacersByRows(index, numberOfRows);
+
+ /*
+ * TODO: this method should probably only add physical rows, and not
+ * populate them - let everything be populated as appropriate by the
+ * logic that follows.
+ *
+ * This also would lead to the fact that paintInsertRows wouldn't
+ * need to return anything.
+ */
+ final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
+ index, numberOfRows);
+
+ /*
+ * insertRows will always change the number of rows - update the
+ * scrollbar sizes.
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+
+ final boolean addedRowsAboveCurrentViewport = index
+ * getDefaultRowHeight() < getScrollTop();
+ final boolean addedRowsBelowCurrentViewport = index
+ * getDefaultRowHeight() > getScrollTop()
+ + getHeightOfSection();
+
+ if (addedRowsAboveCurrentViewport) {
+ /*
+ * We need to tweak the virtual viewport (scroll handle
+ * positions, table "scroll position" and row locations), but
+ * without re-evaluating any rows.
+ */
+
+ final double yDelta = numberOfRows * getDefaultRowHeight();
+ moveViewportAndContent(yDelta);
+ updateTopRowLogicalIndex(numberOfRows);
+ }
+
+ else if (addedRowsBelowCurrentViewport) {
+ // NOOP, we already recalculated scrollbars.
+ }
+
+ else { // some rows were added inside the current viewport
+
+ final int unupdatedLogicalStart = index + addedRows.size();
+ final int visualOffset = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+
+ /*
+ * At this point, we have added new escalator rows, if so
+ * needed.
+ *
+ * If more rows were added than the new escalator rows can
+ * account for, we need to start to spin the escalator to update
+ * the remaining rows aswell.
+ */
+ final int rowsStillNeeded = numberOfRows - addedRows.size();
+
+ if (rowsStillNeeded > 0) {
+ final Range unupdatedVisual = convertToVisual(Range
+ .withLength(unupdatedLogicalStart, rowsStillNeeded));
+ final int end = getDomRowCount();
+ final int start = end - unupdatedVisual.length();
+ final int visualTargetIndex = unupdatedLogicalStart
+ - visualOffset;
+ moveAndUpdateEscalatorRows(Range.between(start, end),
+ visualTargetIndex, unupdatedLogicalStart);
+
+ // move the surrounding rows to their correct places.
+ double rowTop = (unupdatedLogicalStart + (end - start))
+ * getDefaultRowHeight();
+
+ // TODO: Get rid of this try/catch block by fixing the
+ // underlying issue. The reason for this erroneous behavior
+ // might be that Escalator actually works 'by mistake', and
+ // the order of operations is, in fact, wrong.
+ try {
+ final ListIterator<TableRowElement> i = visualRowOrder
+ .listIterator(visualTargetIndex + (end - start));
+
+ int logicalRowIndexCursor = unupdatedLogicalStart;
+ while (i.hasNext()) {
+ rowTop += spacerContainer
+ .getSpacerHeight(logicalRowIndexCursor++);
+
+ final TableRowElement tr = i.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += getDefaultRowHeight();
+ }
+ } catch (Exception e) {
+ Logger logger = getLogger();
+ logger.warning("Ignored out-of-bounds row element access");
+ logger.warning("Escalator state: start=" + start
+ + ", end=" + end + ", visualTargetIndex="
+ + visualTargetIndex
+ + ", visualRowOrder.size()="
+ + visualRowOrder.size());
+ logger.warning(e.toString());
+ }
+ }
+
+ fireRowVisibilityChangeEvent();
+ sortDomElements();
+ }
+ }
+
+ /**
+ * Move escalator rows around, and make sure everything gets
+ * appropriately repositioned and repainted.
+ *
+ * @param visualSourceRange
+ * the range of rows to move to a new place
+ * @param visualTargetIndex
+ * the visual index where the rows will be placed to
+ * @param logicalTargetIndex
+ * the logical index to be assigned to the first moved row
+ */
+ private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
+ final int visualTargetIndex, final int logicalTargetIndex)
+ throws IllegalArgumentException {
+
+ if (visualSourceRange.isEmpty()) {
+ return;
+ }
+
+ assert visualSourceRange.getStart() >= 0 : "Visual source start "
+ + "must be 0 or greater (was "
+ + visualSourceRange.getStart() + ")";
+
+ assert logicalTargetIndex >= 0 : "Logical target must be 0 or "
+ + "greater (was " + logicalTargetIndex + ")";
+
+ assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was "
+ + visualTargetIndex + ")";
+
+ assert visualTargetIndex <= getDomRowCount() : "Visual target "
+ + "must not be greater than the number of escalator rows (was "
+ + visualTargetIndex + ", escalator rows "
+ + getDomRowCount() + ")";
+
+ assert logicalTargetIndex + visualSourceRange.length() <= getRowCount() : "Logical "
+ + "target leads to rows outside of the data range ("
+ + Range.withLength(logicalTargetIndex,
+ visualSourceRange.length())
+ + " goes beyond "
+ + Range.withLength(0, getRowCount()) + ")";
+
+ /*
+ * Since we move a range into another range, the indices might move
+ * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
+ * the collection), the target range will end up being 8..9, instead
+ * of 10..11.
+ *
+ * This applies only if we move elements forward in the collection,
+ * not backward.
+ */
+ final int adjustedVisualTargetIndex;
+ if (visualSourceRange.getStart() < visualTargetIndex) {
+ adjustedVisualTargetIndex = visualTargetIndex
+ - visualSourceRange.length();
+ } else {
+ adjustedVisualTargetIndex = visualTargetIndex;
+ }
+
+ if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
+
+ /*
+ * Reorder the rows to their correct places within
+ * visualRowOrder (unless rows are moved back to their original
+ * places)
+ */
+
+ /*
+ * TODO [[optimize]]: move whichever set is smaller: the ones
+ * explicitly moved, or the others. So, with 10 escalator rows,
+ * if we are asked to move idx[0..8] to the end of the list,
+ * it's faster to just move idx[9] to the beginning.
+ */
+
+ final List<TableRowElement> removedRows = new ArrayList<TableRowElement>(
+ visualSourceRange.length());
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final TableRowElement tr = visualRowOrder
+ .remove(visualSourceRange.getStart());
+ removedRows.add(tr);
+ }
+ visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
+ }
+
+ { // Refresh the contents of the affected rows
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
+ + visualSourceRange.length(); logicalIndex++) {
+ final TableRowElement tr = iter.next();
+ refreshRow(tr, logicalIndex);
+ }
+ }
+
+ { // Reposition the rows that were moved
+ double newRowTop = getRowTop(logicalTargetIndex);
+
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final TableRowElement tr = iter.next();
+ setRowPosition(tr, 0, newRowTop);
+
+ newRowTop += getDefaultRowHeight();
+ newRowTop += spacerContainer
+ .getSpacerHeight(logicalTargetIndex + i);
+ }
+ }
+ }
+
+ /**
+ * Adjust the scroll position and move the contained rows.
+ * <p>
+ * The difference between using this method and simply scrolling is that
+ * this method "takes the rows and spacers with it" and renders them
+ * appropriately. The viewport may be scrolled any arbitrary amount, and
+ * the contents are moved appropriately, but always snapped into a
+ * plausible place.
+ * <p>
+ * <dl>
+ * <dt>Example 1</dt>
+ * <dd>An Escalator with default row height 20px. Adjusting the scroll
+ * position with 7.5px will move the viewport 7.5px down, but leave the
+ * row where it is.</dd>
+ * <dt>Example 2</dt>
+ * <dd>An Escalator with default row height 20px. Adjusting the scroll
+ * position with 27.5px will move the viewport 27.5px down, and place
+ * the row at 20px.</dd>
+ * </dl>
+ *
+ * @param yDelta
+ * the delta of pixels by which to move the viewport and
+ * content. A positive value moves everything downwards,
+ * while a negative value moves everything upwards
+ */
+ public void moveViewportAndContent(final double yDelta) {
+
+ if (yDelta == 0) {
+ return;
+ }
+
+ double newTop = tBodyScrollTop + yDelta;
+ verticalScrollbar.setScrollPos(newTop);
+
+ final double defaultRowHeight = getDefaultRowHeight();
+ double rowPxDelta = yDelta - (yDelta % defaultRowHeight);
+ int rowIndexDelta = (int) (yDelta / defaultRowHeight);
+ if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) {
+
+ Collection<SpacerContainer.SpacerImpl> spacers = spacerContainer
+ .getSpacersAfterPx(tBodyScrollTop,
+ SpacerInclusionStrategy.PARTIAL);
+ for (SpacerContainer.SpacerImpl spacer : spacers) {
+ spacer.setPositionDiff(0, rowPxDelta);
+ spacer.setRowIndex(spacer.getRow() + rowIndexDelta);
+ }
+
+ for (TableRowElement tr : visualRowOrder) {
+ setRowPosition(tr, 0, getRowTop(tr) + rowPxDelta);
+ }
+ }
+
+ setBodyScrollPosition(tBodyScrollLeft, newTop);
+ }
+
+ /**
+ * Adds new physical escalator rows to the DOM at the given index if
+ * there's still a need for more escalator rows.
+ * <p>
+ * If Escalator already is at (or beyond) max capacity, this method does
+ * nothing to the DOM.
+ *
+ * @param index
+ * the index at which to add new escalator rows.
+ * <em>Note:</em>It is assumed that the index is both the
+ * visual index and the logical index.
+ * @param numberOfRows
+ * the number of rows to add at <code>index</code>
+ * @return a list of the added rows
+ */
+ private List<TableRowElement> fillAndPopulateEscalatorRowsIfNeeded(
+ final int index, final int numberOfRows) {
+
+ final int escalatorRowsStillFit = getMaxEscalatorRowCapacity()
+ - getDomRowCount();
+ final int escalatorRowsNeeded = Math.min(numberOfRows,
+ escalatorRowsStillFit);
+
+ if (escalatorRowsNeeded > 0) {
+
+ final List<TableRowElement> addedRows = paintInsertStaticRows(
+ index, escalatorRowsNeeded);
+ visualRowOrder.addAll(index, addedRows);
+
+ double y = index * getDefaultRowHeight()
+ + spacerContainer.getSpacerHeightsSumUntilIndex(index);
+ for (int i = index; i < visualRowOrder.size(); i++) {
+
+ final TableRowElement tr;
+ if (i - index < addedRows.size()) {
+ tr = addedRows.get(i - index);
+ } else {
+ tr = visualRowOrder.get(i);
+ }
+
+ setRowPosition(tr, 0, y);
+ y += getDefaultRowHeight();
+ y += spacerContainer.getSpacerHeight(i);
+ }
+
+ return addedRows;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ private int getMaxEscalatorRowCapacity() {
+ final int maxEscalatorRowCapacity = (int) Math
+ .ceil(getHeightOfSection() / getDefaultRowHeight()) + 1;
+
+ /*
+ * maxEscalatorRowCapacity can become negative if the headers and
+ * footers start to overlap. This is a crazy situation, but Vaadin
+ * blinks the components a lot, so it's feasible.
+ */
+ return Math.max(0, maxEscalatorRowCapacity);
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+ if (numberOfRows == 0) {
+ return;
+ }
+
+ final Range viewportRange = getVisibleRowRange();
+ final Range removedRowsRange = Range
+ .withLength(index, numberOfRows);
+
+ /*
+ * Removing spacers as the very first step will correct the
+ * scrollbars and row offsets right away.
+ *
+ * TODO: actually, it kinda sounds like a Grid feature that a spacer
+ * would be associated with a particular row. Maybe it would be
+ * better to have a spacer separate from rows, and simply collapse
+ * them if they happen to end up on top of each other. This would
+ * probably make supporting the -1 row pretty easy, too.
+ */
+ spacerContainer.paintRemoveSpacers(removedRowsRange);
+
+ final Range[] partitions = removedRowsRange
+ .partitionWith(viewportRange);
+ final Range removedAbove = partitions[0];
+ final Range removedLogicalInside = partitions[1];
+ final Range removedVisualInside = convertToVisual(removedLogicalInside);
+
+ /*
+ * TODO: extract the following if-block to a separate method. I'll
+ * leave this be inlined for now, to make linediff-based code
+ * reviewing easier. Probably will be moved in the following patch
+ * set.
+ */
+
+ /*
+ * Adjust scroll position in one of two scenarios:
+ *
+ * 1) Rows were removed above. Then we just need to adjust the
+ * scrollbar by the height of the removed rows.
+ *
+ * 2) There are no logical rows above, and at least the first (if
+ * not more) visual row is removed. Then we need to snap the scroll
+ * position to the first visible row (i.e. reset scroll position to
+ * absolute 0)
+ *
+ * The logic is optimized in such a way that the
+ * moveViewportAndContent is called only once, to avoid extra
+ * reflows, and thus the code might seem a bit obscure.
+ */
+ final boolean firstVisualRowIsRemoved = !removedVisualInside
+ .isEmpty() && removedVisualInside.getStart() == 0;
+
+ if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
+ final double yDelta = removedAbove.length()
+ * getDefaultRowHeight();
+ final double firstLogicalRowHeight = getDefaultRowHeight();
+ final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
+ .getScrollPos() - yDelta < firstLogicalRowHeight;
+
+ if (removedVisualInside.isEmpty()
+ && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) {
+ /*
+ * rows were removed from above the viewport, so all we need
+ * to do is to adjust the scroll position to account for the
+ * removed rows
+ */
+ moveViewportAndContent(-yDelta);
+ } else if (removalScrollsToShowFirstLogicalRow) {
+ /*
+ * It seems like we've removed all rows from above, and also
+ * into the current viewport. This means we'll need to even
+ * out the scroll position to exactly 0 (i.e. adjust by the
+ * current negative scrolltop, presto!), so that it isn't
+ * aligned funnily
+ */
+ moveViewportAndContent(-verticalScrollbar.getScrollPos());
+ }
+ }
+
+ // ranges evaluated, let's do things.
+ if (!removedVisualInside.isEmpty()) {
+ int escalatorRowCount = body.getDomRowCount();
+
+ /*
+ * remember: the rows have already been subtracted from the row
+ * count at this point
+ */
+ int rowsLeft = getRowCount();
+ if (rowsLeft < escalatorRowCount) {
+ int escalatorRowsToRemove = escalatorRowCount - rowsLeft;
+ for (int i = 0; i < escalatorRowsToRemove; i++) {
+ final TableRowElement tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+
+ paintRemoveRow(tr, index);
+ removeRowPosition(tr);
+ }
+ escalatorRowCount -= escalatorRowsToRemove;
+
+ /*
+ * Because we're removing escalator rows, we don't have
+ * anything to scroll by. Let's make sure the viewport is
+ * scrolled to top, to render any rows possibly left above.
+ */
+ body.setBodyScrollPosition(tBodyScrollLeft, 0);
+
+ /*
+ * We might have removed some rows from the middle, so let's
+ * make sure we're not left with any holes. Also remember:
+ * visualIndex == logicalIndex applies now.
+ */
+ final int dirtyRowsStart = removedLogicalInside.getStart();
+ double y = getRowTop(dirtyRowsStart);
+ for (int i = dirtyRowsStart; i < escalatorRowCount; i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, y);
+ y += getDefaultRowHeight();
+ y += spacerContainer.getSpacerHeight(i);
+ }
+
+ /*
+ * this is how many rows appeared into the viewport from
+ * below
+ */
+ final int rowsToUpdateDataOn = numberOfRows
+ - escalatorRowsToRemove;
+ final int start = Math.max(0, escalatorRowCount
+ - rowsToUpdateDataOn);
+ final int end = escalatorRowCount;
+ for (int i = start; i < end; i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ refreshRow(tr, i);
+ }
+ }
+
+ else {
+ // No escalator rows need to be removed.
+
+ /*
+ * Two things (or a combination thereof) can happen:
+ *
+ * 1) We're scrolled to the bottom, the last rows are
+ * removed. SOLUTION: moveAndUpdateEscalatorRows the
+ * bottommost rows, and place them at the top to be
+ * refreshed.
+ *
+ * 2) We're scrolled somewhere in the middle, arbitrary rows
+ * are removed. SOLUTION: moveAndUpdateEscalatorRows the
+ * removed rows, and place them at the bottom to be
+ * refreshed.
+ *
+ * Since a combination can also happen, we need to handle
+ * this in a smart way, all while avoiding
+ * double-refreshing.
+ */
+
+ final double contentBottom = getRowCount()
+ * getDefaultRowHeight();
+ final double viewportBottom = tBodyScrollTop
+ + getHeightOfSection();
+ if (viewportBottom <= contentBottom) {
+ /*
+ * We're in the middle of the row container, everything
+ * is added to the bottom
+ */
+ paintRemoveRowsAtMiddle(removedLogicalInside,
+ removedVisualInside, 0);
+ }
+
+ else if (removedVisualInside.contains(0)
+ && numberOfRows >= visualRowOrder.size()) {
+ /*
+ * We're removing so many rows that the viewport is
+ * pushed up more than a screenful. This means we can
+ * simply scroll up and everything will work without a
+ * sweat.
+ */
+
+ double left = horizontalScrollbar.getScrollPos();
+ double top = contentBottom - visualRowOrder.size()
+ * getDefaultRowHeight();
+ setBodyScrollPosition(left, top);
+
+ Range allEscalatorRows = Range.withLength(0,
+ visualRowOrder.size());
+ int logicalTargetIndex = getRowCount()
+ - allEscalatorRows.length();
+ moveAndUpdateEscalatorRows(allEscalatorRows, 0,
+ logicalTargetIndex);
+
++ /*
++ * moveAndUpdateEscalatorRows recalculates the rows, but
++ * logical top row index bookkeeping is handled in this
++ * method.
++ *
++ * TODO: Redesign how to keep it easy to track this.
++ */
++ updateTopRowLogicalIndex(-removedLogicalInside.length());
++
+ /*
+ * Scrolling the body to the correct location will be
+ * fixed automatically. Because the amount of rows is
+ * decreased, the viewport is pushed up as the scrollbar
+ * shrinks. So no need to do anything there.
+ *
+ * TODO [[optimize]]: This might lead to a double body
+ * refresh. Needs investigation.
+ */
+ }
+
+ else if (contentBottom
+ + (numberOfRows * getDefaultRowHeight())
+ - viewportBottom < getDefaultRowHeight()) {
+ /*
+ * We're at the end of the row container, everything is
+ * added to the top.
+ */
+
+ /*
+ * FIXME [[spacer]]: above if-clause is coded to only
+ * work with default row heights - will not work with
+ * variable row heights
+ */
+
+ paintRemoveRowsAtBottom(removedLogicalInside,
+ removedVisualInside);
+ updateTopRowLogicalIndex(-removedLogicalInside.length());
+ }
+
+ else {
+ /*
+ * We're in a combination, where we need to both scroll
+ * up AND show new rows at the bottom.
+ *
+ * Example: Scrolled down to show the second to last
+ * row. Remove two. Viewport scrolls up, revealing the
+ * row above row. The last element collapses up and into
+ * view.
+ *
+ * Reminder: this use case handles only the case when
+ * there are enough escalator rows to still render a
+ * full view. I.e. all escalator rows will _always_ be
+ * populated
+ */
+ /*-
+ * 1 1 |1| <- newly rendered
+ * |2| |2| |2|
+ * |3| ==> |*| ==> |5| <- newly rendered
+ * |4| |*|
+ * 5 5
+ *
+ * 1 1 |1| <- newly rendered
+ * |2| |*| |4|
+ * |3| ==> |*| ==> |5| <- newly rendered
+ * |4| |4|
+ * 5 5
+ */
+
+ /*
+ * STEP 1:
+ *
+ * reorganize deprecated escalator rows to bottom, but
+ * don't re-render anything yet
+ */
+ /*-
+ * 1 1 1
+ * |2| |*| |4|
+ * |3| ==> |*| ==> |*|
+ * |4| |4| |*|
+ * 5 5 5
+ */
+ double newTop = getRowTop(visualRowOrder
+ .get(removedVisualInside.getStart()));
+ for (int i = 0; i < removedVisualInside.length(); i++) {
+ final TableRowElement tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+ visualRowOrder.addLast(tr);
+ }
+
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, (int) newTop);
+ newTop += getDefaultRowHeight();
+ newTop += spacerContainer.getSpacerHeight(i
+ + removedLogicalInside.getStart());
+ }
+
+ /*
+ * STEP 2:
+ *
+ * manually scroll
+ */
+ /*-
+ * 1 |1| <-- newly rendered (by scrolling)
+ * |4| |4|
+ * |*| ==> |*|
+ * |*|
+ * 5 5
+ */
+ final double newScrollTop = contentBottom
+ - getHeightOfSection();
+ setScrollTop(newScrollTop);
+ /*
+ * Manually call the scroll handler, so we get immediate
+ * effects in the escalator.
+ */
+ scroller.onScroll();
+
+ /*
+ * Move the bottommost (n+1:th) escalator row to top,
+ * because scrolling up doesn't handle that for us
+ * automatically
+ */
+ moveAndUpdateEscalatorRows(
+ Range.withOnly(escalatorRowCount - 1),
+ 0,
+ getLogicalRowIndex(visualRowOrder.getFirst()) - 1);
+ updateTopRowLogicalIndex(-1);
+
+ /*
+ * STEP 3:
+ *
+ * update remaining escalator rows
+ */
+ /*-
+ * |1| |1|
+ * |4| ==> |4|
+ * |*| |5| <-- newly rendered
+ *
+ * 5
+ */
+
+ final int rowsScrolled = (int) (Math
+ .ceil((viewportBottom - contentBottom)
+ / getDefaultRowHeight()));
+ final int start = escalatorRowCount
+ - (removedVisualInside.length() - rowsScrolled);
+ final Range visualRefreshRange = Range.between(start,
+ escalatorRowCount);
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst()) + start;
+ // in-place move simply re-renders the rows.
+ moveAndUpdateEscalatorRows(visualRefreshRange, start,
+ logicalTargetIndex);
+ }
+ }
+
+ fireRowVisibilityChangeEvent();
+ sortDomElements();
+ }
+
+ updateTopRowLogicalIndex(-removedAbove.length());
+
+ /*
+ * this needs to be done after the escalator has been shrunk down,
+ * or it won't work correctly (due to setScrollTop invocation)
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
+ final Range removedVisualInside, final int logicalOffset) {
+ /*-
+ * : : :
+ * |2| |2| |2|
+ * |3| ==> |*| ==> |4|
+ * |4| |4| |6| <- newly rendered
+ * : : :
+ */
+
+ final int escalatorRowCount = visualRowOrder.size();
+
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getLast())
+ - (removedVisualInside.length() - 1)
+ + logicalOffset;
+ moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
+ logicalTargetIndex);
+
+ // move the surrounding rows to their correct places.
+ final ListIterator<TableRowElement> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getStart());
+
+ double rowTop = getRowTop(removedLogicalInside.getStart()
+ + logicalOffset);
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount
+ - removedVisualInside.length(); i++) {
+ final TableRowElement tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += getDefaultRowHeight();
+ rowTop += spacerContainer.getSpacerHeight(i
+ + removedLogicalInside.getStart());
+ }
+ }
+
+ private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
+ final Range removedVisualInside) {
+ /*-
+ * :
+ * : : |4| <- newly rendered
+ * |5| |5| |5|
+ * |6| ==> |*| ==> |7|
+ * |7| |7|
+ */
+
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst()) - removedVisualInside.length();
+ moveAndUpdateEscalatorRows(removedVisualInside, 0,
+ logicalTargetIndex);
+
+ // move the surrounding rows to their correct places.
+ int firstUpdatedIndex = removedVisualInside.getEnd();
+ final ListIterator<TableRowElement> iterator = visualRowOrder
+ .listIterator(firstUpdatedIndex);
+
+ double rowTop = getRowTop(removedLogicalInside.getStart());
+ int i = 0;
+ while (iterator.hasNext()) {
+ final TableRowElement tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += getDefaultRowHeight();
+ rowTop += spacerContainer.getSpacerHeight(firstUpdatedIndex
+ + i++);
+ }
+ }
+
+ @Override
+ protected int getLogicalRowIndex(final TableRowElement tr) {
+ assert tr.getParentNode() == root : "The given element isn't a row element in the body";
+ int internalIndex = visualRowOrder.indexOf(tr);
+ return getTopRowLogicalIndex() + internalIndex;
+ }
+
+ @Override
+ protected void recalculateSectionHeight() {
+ // NOOP for body, since it doesn't make any sense.
+ }
+
+ /**
+ * Adjusts the row index and number to be relevant for the current
+ * virtual viewport.
+ * <p>
+ * It converts a logical range of rows index to the matching visual
+ * range, truncating the resulting range with the viewport.
+ * <p>
+ * <ul>
+ * <li>Escalator contains logical rows 0..100
+ * <li>Current viewport showing logical rows 20..29
+ * <li>convertToVisual([20..29]) → [0..9]
+ * <li>convertToVisual([15..24]) → [0..4]
+ * <li>convertToVisual([25..29]) → [5..9]
+ * <li>convertToVisual([26..39]) → [6..9]
+ * <li>convertToVisual([0..5]) → [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([35..1]) → [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([0..100]) → [0..9]
+ * </ul>
+ *
+ * @return a logical range converted to a visual range, truncated to the
+ * current viewport. The first visual row has the index 0.
+ */
+ private Range convertToVisual(final Range logicalRange) {
+
+ if (logicalRange.isEmpty()) {
+ return logicalRange;
+ } else if (visualRowOrder.isEmpty()) {
+ // empty range
+ return Range.withLength(0, 0);
+ }
+
+ /*
+ * TODO [[spacer]]: these assumptions will be totally broken with
+ * spacers.
+ */
+ final int maxEscalatorRows = getMaxEscalatorRowCapacity();
+ final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+
+ final Range[] partitions = logicalRange.partitionWith(Range
+ .withLength(currentTopRowIndex, maxEscalatorRows));
+ final Range insideRange = partitions[1];
+ return insideRange.offsetBy(-currentTopRowIndex);
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "td";
+ }
+
+ @Override
+ protected double getHeightOfSection() {
+ final int tableHeight = tableWrapper.getOffsetHeight();
+ final double footerHeight = footer.getHeightOfSection();
+ final double headerHeight = header.getHeightOfSection();
+
+ double heightOfSection = tableHeight - footerHeight - headerHeight;
+ return Math.max(0, heightOfSection);
+ }
+
+ @Override
+ protected void refreshCells(Range logicalRowRange, Range colRange) {
+ Profiler.enter("Escalator.BodyRowContainer.refreshRows");
+
+ final Range visualRange = convertToVisual(logicalRowRange);
+
+ if (!visualRange.isEmpty()) {
+ final int firstLogicalRowIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+ for (int rowNumber = visualRange.getStart(); rowNumber < visualRange
+ .getEnd(); rowNumber++) {
+ refreshRow(visualRowOrder.get(rowNumber),
+ firstLogicalRowIndex + rowNumber, colRange);
+ }
+ }
+
+ Profiler.leave("Escalator.BodyRowContainer.refreshRows");
+ }
+
+ @Override
+ protected TableRowElement getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < visualRowOrder.size()) {
+ return visualRowOrder.get(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+
+ @Override
+ public TableRowElement getRowElement(int index) {
+ if (index < 0 || index >= getRowCount()) {
+ throw new IndexOutOfBoundsException("No such logical index: "
+ + index);
+ }
+ int visualIndex = index
+ - getLogicalRowIndex(visualRowOrder.getFirst());
+ if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) {
+ return super.getRowElement(visualIndex);
+ } else {
+ throw new IllegalStateException("Row with logical index "
+ + index + " is currently not available in the DOM");
+ }
+ }
+
+ private void setBodyScrollPosition(final double scrollLeft,
+ final double scrollTop) {
+ tBodyScrollLeft = scrollLeft;
+ tBodyScrollTop = scrollTop;
+ position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
+ position.set(spacerDecoContainer, 0, -tBodyScrollTop);
+ }
+
+ /**
+ * Make sure that there is a correct amount of escalator rows: Add more
+ * if needed, or remove any superfluous ones.
+ * <p>
+ * This method should be called when e.g. the height of the Escalator
+ * changes.
+ * <p>
+ * <em>Note:</em> This method will make sure that the escalator rows are
+ * placed in the proper places. By default new rows are added below, but
+ * if the content is scrolled down, the rows are populated on top
+ * instead.
+ */
+ public void verifyEscalatorCount() {
+ /*
+ * This method indeed has a smell very similar to paintRemoveRows
+ * and paintInsertRows.
+ *
+ * Unfortunately, those the code can't trivially be shared, since
+ * there are some slight differences in the respective
+ * responsibilities. The "paint" methods fake the addition and
+ * removal of rows, and make sure to either push existing data out
+ * of view, or draw new data into view. Only in some special cases
+ * will the DOM element count change.
+ *
+ * This method, however, has the explicit responsibility to verify
+ * that when "something" happens, we still have the correct amount
+ * of escalator rows in the DOM, and if not, we make sure to modify
+ * that count. Only in some special cases do we need to take into
+ * account other things than simply modifying the DOM element count.
+ */
+
+ Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
+
+ if (!isAttached()) {
+ return;
+ }
+
+ final int maxEscalatorRows = getMaxEscalatorRowCapacity();
+ final int neededEscalatorRows = Math.min(maxEscalatorRows,
+ body.getRowCount());
+ final int neededEscalatorRowsDiff = neededEscalatorRows
+ - visualRowOrder.size();
+
+ if (neededEscalatorRowsDiff > 0) {
+ // needs more
+
+ /*
+ * This is a workaround for the issue where we might be scrolled
+ * to the bottom, and the widget expands beyond the content
+ * range
+ */
+
+ final int index = visualRowOrder.size();
+ final int nextLastLogicalIndex;
+ if (!visualRowOrder.isEmpty()) {
+ nextLastLogicalIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ } else {
+ nextLastLogicalIndex = 0;
+ }
+
+ final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
+ - neededEscalatorRowsDiff;
+ if (contentWillFit) {
+ final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
+ index, neededEscalatorRowsDiff);
+
+ /*
+ * Since fillAndPopulateEscalatorRowsIfNeeded operates on
+ * the assumption that index == visual index == logical
+ * index, we thank for the added escalator rows, but since
+ * they're painted in the wrong CSS position, we need to
+ * move them to their actual locations.
+ *
+ * Note: this is the second (see body.paintInsertRows)
+ * occasion where fillAndPopulateEscalatorRowsIfNeeded would
+ * behave "more correctly" if it only would add escalator
+ * rows to the DOM and appropriate bookkeping, and not
+ * actually populate them :/
+ */
+ moveAndUpdateEscalatorRows(
+ Range.withLength(index, addedRows.size()), index,
+ nextLastLogicalIndex);
+ } else {
+ /*
+ * TODO [[optimize]]
+ *
+ * We're scrolled so far down that all rows can't be simply
+ * appended at the end, since we might start displaying
+ * escalator rows that don't exist. To avoid the mess that
+ * is body.paintRemoveRows, this is a dirty hack that dumbs
+ * the problem down to a more basic and already-solved
+ * problem:
+ *
+ * 1) scroll all the way up 2) add the missing escalator
+ * rows 3) scroll back to the original position.
+ *
+ * Letting the browser scroll back to our original position
+ * will automatically solve any possible overflow problems,
+ * since the browser will not allow us to scroll beyond the
+ * actual content.
+ */
+
+ final double oldScrollTop = getScrollTop();
+ setScrollTop(0);
+ scroller.onScroll();
+ fillAndPopulateEscalatorRowsIfNeeded(index,
+ neededEscalatorRowsDiff);
+ setScrollTop(oldScrollTop);
+ scroller.onScroll();
+ }
+ }
+
+ else if (neededEscalatorRowsDiff < 0) {
+ // needs less
+
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(visualRowOrder.size());
+ for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
+ final Element last = iter.previous();
+ last.removeFromParent();
+ iter.remove();
+ }
+
+ /*
+ * If we were scrolled to the bottom so that we didn't have an
+ * extra escalator row at the bottom, we'll probably end up with
+ * blank space at the bottom of the escalator, and one extra row
+ * above the header.
+ *
+ * Experimentation idea #1: calculate "scrollbottom" vs content
+ * bottom and remove one row from top, rest from bottom. This
+ * FAILED, since setHeight has already happened, thus we never
+ * will detect ourselves having been scrolled all the way to the
+ * bottom.
+ */
+
+ if (!visualRowOrder.isEmpty()) {
+ final double firstRowTop = getRowTop(visualRowOrder
+ .getFirst());
+ final double firstRowMinTop = tBodyScrollTop
+ - getDefaultRowHeight();
+ if (firstRowTop < firstRowMinTop) {
+ final int newLogicalIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ moveAndUpdateEscalatorRows(Range.withOnly(0),
+ visualRowOrder.size(), newLogicalIndex);
+ }
+ }
+ }
+
+ if (neededEscalatorRowsDiff != 0) {
+ fireRowVisibilityChangeEvent();
+ }
+
+ Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
+ }
+
+ @Override
+ protected void reapplyDefaultRowHeights() {
+ if (visualRowOrder.isEmpty()) {
+ return;
+ }
+
+ Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
+
+ /* step 1: resize and reposition rows */
+ for (int i = 0; i < visualRowOrder.size(); i++) {
+ TableRowElement tr = visualRowOrder.get(i);
+ reapplyRowHeight(tr, getDefaultRowHeight());
+
+ final int logicalIndex = getTopRowLogicalIndex() + i;
+ setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight());
+ }
+
+ /*
+ * step 2: move scrollbar so that it corresponds to its previous
+ * place
+ */
+
+ /*
+ * This ratio needs to be calculated with the scrollsize (not max
+ * scroll position) in order to align the top row with the new
+ * scroll position.
+ */
+ double scrollRatio = verticalScrollbar.getScrollPos()
+ / verticalScrollbar.getScrollSize();
+ scroller.recalculateScrollbarsForVirtualViewport();
+ verticalScrollbar.setScrollPos((int) (getDefaultRowHeight()
+ * getRowCount() * scrollRatio));
+ setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
+ verticalScrollbar.getScrollPos());
+ scroller.onScroll();
+
+ /* step 3: make sure we have the correct amount of escalator rows. */
+ verifyEscalatorCount();
+
+ int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) / getDefaultRowHeight());
+ setTopRowLogicalIndex(logicalLogical);
+
+ Profiler.leave("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
+ }
+
+ /**
+ * Sorts the rows in the DOM to correspond to the visual order.
+ *
+ * @see #visualRowOrder
+ */
+ private void sortDomElements() {
+ final String profilingName = "Escalator.BodyRowContainer.sortDomElements";
+ Profiler.enter(profilingName);
+
+ /*
+ * Focus is lost from an element if that DOM element is (or any of
+ * its parents are) removed from the document. Therefore, we sort
+ * everything around that row instead.
+ */
+ final TableRowElement focusedRow = getRowWithFocus();
+
+ if (focusedRow != null) {
+ assert focusedRow.getParentElement() == root : "Trying to sort around a row that doesn't exist in body";
+ assert visualRowOrder.contains(focusedRow)
+ || body.spacerContainer.isSpacer(focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder or is not a spacer.";
+ }
+
+ /*
+ * Two cases handled simultaneously:
+ *
+ * 1) No focus on rows. We iterate visualRowOrder backwards, and
+ * take the respective element in the DOM, and place it as the first
+ * child in the body element. Then we take the next-to-last from
+ * visualRowOrder, and put that first, pushing the previous row as
+ * the second child. And so on...
+ *
+ * 2) Focus on some row within Escalator body. Again, we iterate
+ * visualRowOrder backwards. This time, we use the focused row as a
+ * pivot: Instead of placing rows from the bottom of visualRowOrder
+ * and placing it first, we place it underneath the focused row.
+ * Once we hit the focused row, we don't move it (to not reset
+ * focus) but change sorting mode. After that, we place all rows as
+ * the first child.
+ */
+
+ List<TableRowElement> orderedBodyRows = new ArrayList<TableRowElement>(
+ visualRowOrder);
+ Map<Integer, SpacerContainer.SpacerImpl> spacers = body.spacerContainer
+ .getSpacers();
+
+ /*
+ * Start at -1 to include a spacer that is rendered above the
+ * viewport, but its parent row is still not shown
+ */
+ for (int i = -1; i < visualRowOrder.size(); i++) {
+ SpacerContainer.SpacerImpl spacer = spacers.remove(Integer
+ .valueOf(getTopRowLogicalIndex() + i));
+
+ if (spacer != null) {
+ orderedBodyRows.add(i + 1, spacer.getRootElement());
+ spacer.show();
+ }
+ }
+ /*
+ * At this point, invisible spacers aren't reordered, so their
+ * position in the DOM will remain undefined.
+ */
+
+ // If a spacer was not reordered, it means that it's out of view.
+ for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) {
+ unmovedSpacer.hide();
+ }
+
+ /*
+ * If we have a focused row, start in the mode where we put
+ * everything underneath that row. Otherwise, all rows are placed as
+ * first child.
+ */
+ boolean insertFirst = (focusedRow == null);
+
+ final ListIterator<TableRowElement> i = orderedBodyRows
+ .listIterator(orderedBodyRows.size());
+ while (i.hasPrevious()) {
+ TableRowElement tr = i.previous();
+
+ if (tr == focusedRow) {
+ insertFirst = true;
+ } else if (insertFirst) {
+ root.insertFirst(tr);
+ } else {
+ root.insertAfter(tr, focusedRow);
+ }
+ }
+
+ Profiler.leave(profilingName);
+ }
+
+ /**
+ * Get the {@literal <tbody>} row that contains (or has) focus.
+ *
+ * @return The {@literal <tbody>} row that contains a focused DOM
+ * element, or <code>null</code> if focus is outside of a body
+ * row.
+ */
+ private TableRowElement getRowWithFocus() {
+ TableRowElement rowContainingFocus = null;
+
+ final Element focusedElement = WidgetUtil.getFocusedElement();
+
+ if (focusedElement != null && root.isOrHasChild(focusedElement)) {
+ Element e = focusedElement;
+
+ while (e != null && e != root) {
+ /*
+ * You never know if there's several tables embedded in a
+ * cell... We'll take the deepest one.
+ */
+ if (TableRowElement.is(e)) {
+ rowContainingFocus = TableRowElement.as(e);
+ }
+ e = e.getParentElement();
+ }
+ }
+
+ return rowContainingFocus;
+ }
+
+ @Override
+ public Cell getCell(Element element) {
+ Cell cell = super.getCell(element);
+ if (cell == null) {
+ return null;
+ }
+
+ // Convert DOM coordinates to logical coordinates for rows
+ TableRowElement rowElement = (TableRowElement) cell.getElement()
+ .getParentElement();
+ return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
+ cell.getElement());
+ }
+
+ @Override
+ public void setSpacer(int rowIndex, double height)
+ throws IllegalArgumentException {
+ spacerContainer.setSpacer(rowIndex, height);
+ }
+
+ @Override
+ public void setSpacerUpdater(SpacerUpdater spacerUpdater)
+ throws IllegalArgumentException {
+ spacerContainer.setSpacerUpdater(spacerUpdater);
+ }
+
+ @Override
+ public SpacerUpdater getSpacerUpdater() {
+ return spacerContainer.getSpacerUpdater();
+ }
+
+ /**
+ * <em>Calculates</em> the correct top position of a row at a logical
+ * index, regardless if there is one there or not.
+ * <p>
+ * A correct result requires that both {@link #getDefaultRowHeight()} is
+ * consistent, and the placement and height of all spacers above the
+ * given logical index are consistent.
+ *
+ * @param logicalIndex
+ * the logical index of the row for which to calculate the
+ * top position
+ * @return the position at which to place a row in {@code logicalIndex}
+ * @see #getRowTop(TableRowElement)
+ */
+ private double getRowTop(int logicalIndex) {
+ double top = spacerContainer
+ .getSpacerHeightsSumUntilIndex(logicalIndex);
+ return top + (logicalIndex * getDefaultRowHeight());
+ }
+
+ public void shiftRowPositions(int row, double diff) {
+ for (TableRowElement tr : getVisibleRowsAfter(row)) {
+ setRowPosition(tr, 0, getRowTop(tr) + diff);
+ }
+ }
+
+ private List<TableRowElement> getVisibleRowsAfter(int logicalRow) {
+ Range visibleRowLogicalRange = getVisibleRowRange();
+
+ boolean allRowsAreInView = logicalRow < visibleRowLogicalRange
+ .getStart();
+ boolean noRowsAreInView = logicalRow >= visibleRowLogicalRange
+ .getEnd() - 1;
+
+ if (allRowsAreInView) {
+ return Collections.unmodifiableList(visualRowOrder);
+ } else if (noRowsAreInView) {
+ return Collections.emptyList();
+ } else {
+ int fromIndex = (logicalRow - visibleRowLogicalRange.getStart()) + 1;
+ int toIndex = visibleRowLogicalRange.length();
+ List<TableRowElement> sublist = visualRowOrder.subList(
+ fromIndex, toIndex);
+ return Collections.unmodifiableList(sublist);
+ }
+ }
+
+ @Override
+ public int getDomRowCount() {
+ return root.getChildCount()
+ - spacerContainer.getSpacersInDom().size();
+ }
+
+ @Override
+ protected boolean rowCanBeFrozen(TableRowElement tr) {
+ return visualRowOrder.contains(tr);
+ }
+
+ void reapplySpacerWidths() {
+ spacerContainer.reapplySpacerWidths();
+ }
+
+ void scrollToSpacer(int spacerIndex, ScrollDestination destination,
+ int padding) {
+ spacerContainer.scrollToSpacer(spacerIndex, destination, padding);
+ }
+ }
+
+ private class ColumnConfigurationImpl implements ColumnConfiguration {
+ public class Column {
+ public static final double DEFAULT_COLUMN_WIDTH_PX = 100;
+
+ private double definedWidth = -1;
+ private double calculatedWidth = DEFAULT_COLUMN_WIDTH_PX;
+ private boolean measuringRequested = false;
+
+ public void setWidth(double px) {
+ definedWidth = px;
+
+ if (px < 0) {
+ if (isAttached()) {
+ calculateWidth();
+ } else {
+ /*
+ * the column's width is calculated at Escalator.onLoad
+ * via measureAndSetWidthIfNeeded!
+ */
+ measuringRequested = true;
+ }
+ } else {
+ calculatedWidth = px;
+ }
+ }
+
+ public double getDefinedWidth() {
+ return definedWidth;
+ }
+
+ /**
+ * Returns the actual width in the DOM.
+ *
+ * @return the width in pixels in the DOM. Returns -1 if the column
+ * needs measuring, but has not been yet measured
+ */
+ public double getCalculatedWidth() {
+ /*
+ * This might return an untrue value (e.g. during init/onload),
+ * since we haven't had a proper chance to actually calculate
+ * widths yet.
+ *
+ * This is fixed during Escalator.onLoad, by the call to
+ * "measureAndSetWidthIfNeeded", which fixes "everything".
+ */
+ if (!measuringRequested) {
+ return calculatedWidth;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Checks if the column needs measuring, and then measures it.
+ * <p>
+ * Called by {@link Escalator#onLoad()}.
+ */
+ public boolean measureAndSetWidthIfNeeded() {
+ assert isAttached() : "Column.measureAndSetWidthIfNeeded() was called even though Escalator was not attached!";
+
+ if (measuringRequested) {
+ measuringRequested = false;
+ setWidth(definedWidth);
+ return true;
+ }
+ return false;
+ }
+
+ private void calculateWidth() {
+ calculatedWidth = getMaxCellWidth(columns.indexOf(this));
+ }
+ }
+
+ private final List<Column> columns = new ArrayList<Column>();
+ private int frozenColumns = 0;
+
+ /*
+ * TODO: this is a bit of a duplicate functionality with the
+ * Column.calculatedWidth caching. Probably should use one or the other,
+ * not both
+ */
+ /**
+ * A cached array of all the calculated column widths.
+ *
+ * @see #getCalculatedColumnWidths()
+ */
+ private double[] widthsArray = null;
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there are no rows in the DOM when
+ * this method is called.
+ *
+ * @see #hasSomethingInDom()
+ */
+ @Override
+ public void removeColumns(final int index, final int numberOfColumns) {
+ // Validate
+ assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
+
+ // Move the horizontal scrollbar to the left, if removed columns are
+ // to the left of the viewport
+ removeColumnsAdjustScrollbar(index, numberOfColumns);
+
+ // Remove from DOM
+ header.paintRemoveColumns(index, numberOfColumns);
+ body.paintRemoveColumns(index, numberOfColumns);
+ footer.paintRemoveColumns(index, numberOfColumns);
+
+ // Remove from bookkeeping
+ flyweightRow.removeCells(index, numberOfColumns);
+ columns.subList(index, index + numberOfColumns).clear();
+
+ // Adjust frozen columns
+ if (index < getFrozenColumnCount()) {
+ if (index + numberOfColumns < frozenColumns) {
+ /*
+ * Last removed column was frozen, meaning that all removed
+ * columns were frozen. Just decrement the number of frozen
+ * columns accordingly.
+ */
+ frozenColumns -= numberOfColumns;
+ } else {
+ /*
+ * If last removed column was not frozen, we have removed
+ * columns beyond the frozen range, so all remaining frozen
+ * columns are to the left of the removed columns.
+ */
+ frozenColumns = index;
+ }
+ }
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ body.verifyEscalatorCount();
+
+ if (getColumnConfiguration().getColumnCount() > 0) {
+ reapplyRowWidths(header);
+ reapplyRowWidths(body);
+ reapplyRowWidths(footer);
+ }
+
+ /*
+ * Colspans make any kind of automatic clever content re-rendering
+ * impossible: As soon as anything has colspans, removing one might
+ * reveal further colspans, modifying the DOM structure once again,
+ * ending in a cascade of updates. Because we don't know how the
+ * data is updated.
+ *
+ * So, instead, we don't do anything. The client code is responsible
+ * for re-rendering the content (if so desired). Everything Just
+ * Works (TM) if colspans aren't used.
+ */
+ }
+
+ private void reapplyRowWidths(AbstractRowContainer container) {
+ if (container.getRowCount() > 0) {
+ container.reapplyRowWidths();
+ }
+ }
+
+ private void removeColumnsAdjustScrollbar(int index, int numberOfColumns) {
+ if (horizontalScrollbar.getOffsetSize() >= horizontalScrollbar
+ .getScrollSize()) {
+ return;
+ }
+
+ double leftPosOfFirstColumnToRemove = getCalculatedColumnsWidth(Range
+ .between(0, index));
+ double widthOfColumnsToRemove = getCalculatedColumnsWidth(Range
+ .withLength(index, numberOfColumns));
+
+ double scrollLeft = horizontalScrollbar.getScrollPos();
+
+ if (scrollLeft <= leftPosOfFirstColumnToRemove) {
+ /*
+ * viewport is scrolled to the left of the first removed column,
+ * so there's no need to adjust anything
+ */
+ return;
+ }
+
+ double adjustedScrollLeft = Math.max(leftPosOfFirstColumnToRemove,
+ scrollLeft - widthOfColumnsToRemove);
+ horizontalScrollbar.setScrollPos(adjustedScrollLeft);
+ }
+
+ /**
+ * Calculate the width of a row, as the sum of columns' widths.
+ *
+ * @return the width of a row, in pixels
+ */
+ public double calculateRowWidth() {
+ return getCalculatedColumnsWidth(Range.between(0, getColumnCount()));
+ }
+
+ private void assertArgumentsAreValidAndWithinRange(final int index,
+ final int numberOfColumns) {
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns can't be less than 1 (was "
+ + numberOfColumns + ")");
+ }
+
+ if (index < 0 || index + numberOfColumns > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "column range (" + index + ".."
+ + (index + numberOfColumns)
+ + ") was outside of the current "
+ + "number of columns (" + getColumnCount() + ")");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for rows when this
+ * method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void insertColumns(final int index, final int numberOfColumns) {
+ // Validate
+ if (index < 0 || index > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given index(" + index
+ + ") was outside of the current number of columns (0.."
+ + getColumnCount() + ")");
+ }
+
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns must be 1 or greater (was "
+ + numberOfColumns);
+ }
+
+ // Add to bookkeeping
+ flyweightRow.addCells(index, numberOfColumns);
+ for (int i = 0; i < numberOfColumns; i++) {
+ columns.add(index, new Column());
+ }
+
+ // Adjust frozen columns
+ boolean frozen = index < frozenColumns;
+ if (frozen) {
+ frozenColumns += numberOfColumns;
+ }
+
+ // this needs to be before the scrollbar adjustment.
+ boolean scrollbarWasNeeded = horizontalScrollbar.getOffsetSize() < horizontalScrollbar
+ .getScrollSize();
+ scroller.recalculateScrollbarsForVirtualViewport();
+ boolean scrollbarIsNowNeeded = horizontalScrollbar.getOffsetSize() < horizontalScrollbar
+ .getScrollSize();
+ if (!scrollbarWasNeeded && scrollbarIsNowNeeded) {
+ body.verifyEscalatorCount();
+ }
+
+ // Add to DOM
+ header.paintInsertColumns(index, numberOfColumns, frozen);
+ body.paintInsertColumns(index, numberOfColumns, frozen);
+ footer.paintInsertColumns(index, numberOfColumns, frozen);
+
+ // fix initial width
+ if (header.getRowCount() > 0 || body.getRowCount() > 0
+ || footer.getRowCount() > 0) {
+
+ Map<Integer, Double> colWidths = new HashMap<Integer, Double>();
+ Double width = Double.valueOf(Column.DEFAULT_COLUMN_WIDTH_PX);
+ for (int i = index; i < index + numberOfColumns; i++) {
+ Integer col = Integer.valueOf(i);
+ colWidths.put(col, width);
+ }
+ getColumnConfiguration().setColumnWidths(colWidths);
+ }
+
+ // Adjust scrollbar
+ double pixelsToInsertedColumn = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0, index));
+ final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn;
+
+ if (columnsWereAddedToTheLeftOfViewport) {
+ double insertedColumnsWidth = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(index,
+ numberOfColumns));
+ horizontalScrollbar.setScrollPos(scroller.lastScrollLeft
+ + insertedColumnsWidth);
+ }
+
+ /*
+ * Colspans make any kind of automatic clever content re-rendering
+ * impossible: As soon as anything has colspans, adding one might
+ * affect surrounding colspans, modifying the DOM structure once
+ * again, ending in a cascade of updates. Because we don't know how
+ * the data is updated.
+ *
+ * So, instead, we don't do anything. The client code is responsible
+ * for re-rendering the content (if so desired). Everything Just
+ * Works (TM) if colspans aren't used.
+ */
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columns.size();
+ }
+
+ @Override
+ public void setFrozenColumnCount(int count)
+ throws IllegalArgumentException {
+ if (count < 0 || count > getColumnCount()) {
+ throw new IllegalArgumentException(
+ "count must be between 0 and the current number of columns ("
+ + getColumnCount() + ")");
+ }
+ int oldCount = frozenColumns;
+ if (count == oldCount) {
+ return;
+ }
+
+ frozenColumns = count;
+
+ if (hasSomethingInDom()) {
+ // Are we freezing or unfreezing?
+ boolean frozen = count > oldCount;
+
+ int firstAffectedCol;
+ int firstUnaffectedCol;
+
+ if (frozen) {
+ firstAffectedCol = oldCount;
+ firstUnaffectedCol = count;
+ } else {
+ firstAffectedCol = count;
+ firstUnaffectedCol = oldCount;
+ }
+
+ if (oldCount > 0) {
+ header.setColumnLastFrozen(oldCount - 1, false);
+ body.setColumnLastFrozen(oldCount - 1, false);
+ footer.setColumnLastFrozen(oldCount - 1, false);
+ }
+ if (count > 0) {
+ header.setColumnLastFrozen(count - 1, true);
+ body.setColumnLastFrozen(count - 1, true);
+ footer.setColumnLastFrozen(count - 1, true);
+ }
+
+ for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) {
+ header.setColumnFrozen(col, frozen);
+ body.setColumnFrozen(col, frozen);
+ footer.setColumnFrozen(col, frozen);
+ }
+ }
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ @Override
+ public int getFrozenColumnCount() {
+ return frozenColumns;
+ }
+
+ @Override
+ public void setColumnWidth(int index, double px)
+ throws IllegalArgumentException {
+ setColumnWidths(Collections.singletonMap(Integer.valueOf(index),
+ Double.valueOf(px)));
+ }
+
+ @Override
+ public void setColumnWidths(Map<Integer, Double> indexWidthMap)
+ throws IllegalArgumentException {
+
+ if (indexWidthMap == null) {
+ throw new IllegalArgumentException("indexWidthMap was null");
+ }
+
+ if (indexWidthMap.isEmpty()) {
+ return;
+ }
+
+ for (Entry<Integer, Double> entry : indexWidthMap.entrySet()) {
+ int index = entry.getKey().intValue();
+ double width = entry.getValue().doubleValue();
+
+ checkValidColumnIndex(index);
+
+ // Not all browsers will accept any fractional size..
+ width = WidgetUtil.roundSizeDown(width);
+ columns.get(index).setWidth(width);
+
+ }
+
+ widthsArray = null;
+ header.reapplyColumnWidths();
+ body.reapplyColumnWidths();
+ footer.reapplyColumnWidths();
+
+ recalculateElementSizes();
+ }
+
+ private void checkValidColumnIndex(int index)
+ throws IllegalArgumentException {
+ if (!Range.withLength(0, getColumnCount()).contains(index)) {
+ throw new IllegalArgumentException("The given column index ("
+ + index + ") does not exist");
+ }
+ }
+
+ @Override
+ public double getColumnWidth(int index) throws IllegalArgumentException {
+ checkValidColumnIndex(index);
+ return columns.get(index).getDefinedWidth();
+ }
+
+ @Override
+ public double getColumnWidthActual(int index) {
+ return columns.get(index).getCalculatedWidth();
+ }
+
+ private double getMaxCellWidth(int colIndex)
+ throws IllegalArgumentException {
+ double headerWidth = header.measureMinCellWidth(colIndex, true);
+ double bodyWidth = body.measureMinCellWidth(colIndex, true);
+ double footerWidth = footer.measureMinCellWidth(colIndex, true);
+
+ double maxWidth = Math.max(headerWidth,
+ Math.max(bodyWidth, footerWidth));
+ assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible.";
+ return maxWidth;
+ }
+
+ private double getMinCellWidth(int colIndex)
+ throws IllegalArgumentException {
+ double headerWidth = header.measureMinCellWidth(colIndex, false);
+ double bodyWidth = body.measureMinCellWidth(colIndex, false);
+ double footerWidth = footer.measureMinCellWidth(colIndex, false);
+
+ double minWidth = Math.max(headerWidth,
+ Math.max(bodyWidth, footerWidth));
+ assert minWidth >= 0 : "Got a negative max width for a column, which should be impossible.";
+ return minWidth;
+ }
+
+ /**
+ * Calculates the width of the columns in a given range.
+ *
+ * @param columns
+ * the columns to calculate
+ * @return the total width of the columns in the given
+ * <code>columns</code>
+ */
+ double getCalculatedColumnsWidth(final Range columns) {
+ /*
+ * This is an assert instead of an exception, since this is an
+ * internal method.
+ */
+ assert columns.isSubsetOf(Range.between(0, getColumnCount())) : "Range "
+ + "was outside of current column range (i.e.: "
+ + Range.between(0, getColumnCount())
+ + ", but was given :"
+ + columns;
+
+ double sum = 0;
+ for (int i = columns.getStart(); i < columns.getEnd(); i++) {
+ double columnWidthActual = getColumnWidthActual(i);
+ sum += columnWidthActual;
+ }
+ return sum;
+ }
+
+ double[] getCalculatedColumnWidths() {
+ if (widthsArray == null || widthsArray.length != getColumnCount()) {
+ widthsArray = new double[getColumnCount()];
+ for (int i = 0; i < columns.size(); i++) {
+ widthsArray[i] = columns.get(i).getCalculatedWidth();
+ }
+ }
+ return widthsArray;
+ }
+
+ @Override
+ public void refreshColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns must be 1 or greater (was "
+ + numberOfColumns + ")");
+ }
+
+ if (index < 0 || index + numberOfColumns > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "column range (" + index + ".."
+ + (index + numberOfColumns)
+ + ") was outside of the current number of columns ("
+ + getColumnCount() + ")");
+ }
+
+ header.refreshColumns(index, numberOfColumns);
+ body.refreshColumns(index, numberOfColumns);
+ footer.refreshColumns(index, numberOfColumns);
+ }
+ }
+
+ /**
+ * A decision on how to measure a spacer when it is partially within a
+ * designated range.
+ * <p>
+ * The meaning of each value may differ depending on the context it is being
+ * used in. Check that particular method's JavaDoc.
+ */
+ private enum SpacerInclusionStrategy {
+ /** A representation of "the entire spacer". */
+ COMPLETE,
+
+ /** A representation of "a partial spacer". */
+ PARTIAL,
+
+ /** A representation of "no spacer at all". */
+ NONE
+ }
+
+ private class SpacerContainer {
+
+ /** This is used mainly for testing purposes */
+ private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow";
+
+ private final class SpacerImpl implements Spacer {
+ private TableCellElement spacerElement;
+ private TableRowElement root;
+ private DivElement deco;
+ private int rowIndex;
+ private double height = -1;
+ private boolean domHasBeenSetup = false;
+ private double decoHeight;
+ private double defaultCellBorderBottomSize = -1;
+
+ public SpacerImpl(int rowIndex) {
+ this.rowIndex = rowIndex;
+
+ root = TableRowElement.as(DOM.createTR());
+ spacerElement = TableCellElement.as(DOM.createTD());
+ root.appendChild(spacerElement);
+ root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
+ deco = DivElement.as(DOM.createDiv());
+ }
+
+ public void setPositionDiff(double x, double y) {
+ setPosition(getLeft() + x, getTop() + y);
+ }
+
+ public void setupDom(double height) {
+ assert !domHasBeenSetup : "DOM can't be set up twice.";
+ assert RootPanel.get().getElement().isOrHasChild(root) : "Root element should've been attached to the DOM by now.";
+ domHasBeenSetup = true;
+
+ getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX);
+ setHeight(height);
+
+ spacerElement.setColSpan(getColumnConfiguration()
+ .getColumnCount());
+
+ setStylePrimaryName(getStylePrimaryName());
+ }
+
+ public TableRowElement getRootElement() {
+ return root;
+ }
+
+ @Override
+ public Element getDecoElement() {
+ return deco;
+ }
+
+ public void setPosition(double x, double y) {
+ positions.set(getRootElement(), x, y);
+ positions
+ .set(getDecoElement(), 0, y - getSpacerDecoTopOffset());
+ }
+
+ private double getSpacerDecoTopOffset() {
+ return getBody().getDefaultRowHeight();
+ }
+
+ public void setStylePrimaryName(String style) {
+ UIObject.setStylePrimaryName(root, style + "-spacer");
+ UIObject.setStylePrimaryName(deco, style + "-spacer-deco");
+ }
+
+ public void setHeight(double height) {
+
+ assert height >= 0 : "Height must be more >= 0 (was " + height
+ + ")";
+
+ final double heightDiff = height - Math.max(0, this.height);
+ final double oldHeight = this.height;
+
+ this.height = height;
+
+ // since the spacer might be rendered on top of the previous
+ // rows border (done with css), need to increase height the
+ // amount of the border thickness
+ if (defaultCellBorderBottomSize < 0) {
+ defaultCellBorderBottomSize = WidgetUtil
+ .getBorderBottomThickness(body.getRowElement(
+ getVisibleRowRange().getStart())
+ .getFirstChildElement());
+ }
+ root.getStyle().setHeight(height + defaultCellBorderBottomSize,
+ Unit.PX);
+
+ // move the visible spacers getRow row onwards.
+ shiftSpacerPositionsAfterRow(getRow(), heightDiff);
+
+ /*
+ * If we're growing, we'll adjust the scroll size first, then
+ * adjust scrolling. If we're shrinking, we do it after the
+ * second if-clause.
+ */
+ boolean spacerIsGrowing = heightDiff > 0;
+ if (spacerIsGrowing) {
+ verticalScrollbar.setScrollSize(verticalScrollbar
+ .getScrollSize() + heightDiff);
+ }
+
+ /*
+ * Don't modify the scrollbars if we're expanding the -1 spacer
+ * while we're scrolled to the top.
+ */
+ boolean minusOneSpacerException = spacerIsGrowing
+ && getRow() == -1 && body.getTopRowLogicalIndex() == 0;
+
+ boolean viewportNeedsScrolling = getRow() < body
+ .getTopRowLogicalIndex() && !minusOneSpacerException;
+ if (viewportNeedsScrolling) {
+
+ /*
+ * We can't use adjustScrollPos here, probably because of a
+ * bookkeeping-related race condition.
+ *
+ * This particular situation is easier, however, since we
+ * know exactly how many pixels we need to move (heightDiff)
+ * and all elements below the spacer always need to move
+ * that pixel amount.
+ */
+
+ for (TableRowElement row : body.visualRowOrder) {
+ body.setRowPosition(row, 0, body.getRowTop(row)
+ + heightDiff);
+ }
+
+ double top = getTop();
+ double bottom = top + oldHeight;
+ double scrollTop = verticalScrollbar.getScrollPos();
+
+ boolean viewportTopIsAtMidSpacer = top < scrollTop
+ && scrollTop < bottom;
+
+ final double moveDiff;
+ if (viewportTopIsAtMidSpacer && !spacerIsGrowing) {
+
+ /*
+ * If the scroll top is in the middle of the modified
+ * spacer, we want to scroll the viewport up as usual,
+ * but we don't want to scroll past the top of it.
+ *
+ * Math.max ensures this (remember: the result is going
+ * to be negative).
+ */
+
+ moveDiff = Math.max(heightDiff, top - scrollTop);
+ } else {
+ moveDiff = heightDiff;
+ }
+ body.setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop
+ + moveDiff);
+ verticalScrollbar.setScrollPosByDelta(moveDiff);
+
+ } else {
+ body.shiftRowPositions(getRow(), heightDiff);
+ }
+
+ if (!spacerIsGrowing) {
+ verticalScrollbar.setScrollSize(verticalScrollbar
+ .getScrollSize() + heightDiff);
+ }
+
+ updateDecoratorGeometry(height);
+ }
+
+ /** Resizes and places the decorator. */
+ private void updateDecoratorGeometry(double detailsHeight) {
+ Style style = deco.getStyle();
+ decoHeight = detailsHeight + getBody().getDefaultRowHeight();
+ style.setHeight(decoHeight, Unit.PX);
+ }
+
+ @Override
+ public Element getElement() {
+ return spacerElement;
+ }
+
+ @Override
+ public int getRow() {
+ return rowIndex;
+ }
+
+ public double getHeight() {
+ assert height >= 0 : "Height was not previously set by setHeight.";
+ return height;
+ }
+
+ public double getTop() {
+ return positions.getTop(getRootElement());
+ }
+
+ public double getLeft() {
+ return positions.getLeft(getRootElement());
+ }
+
+ /**
+ * Sets a new row index for this spacer. Also updates the bookeeping
+ * at {@link SpacerContainer#rowIndexToSpacer}.
+ */
+ @SuppressWarnings("boxing")
+ public void setRowIndex(int rowIndex) {
+ SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex);
+ assert this == spacer : "trying to move an unexpected spacer.";
+ this.rowIndex = rowIndex;
+ root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
+ rowIndexToSpacer.put(this.rowIndex, this);
+ }
+
+ /**
+ * Updates the spacer's visibility parameters, based on whether it
+ * is being currently visible or not.
+ */
+ public void updateVisibility() {
+ if (isInViewport()) {
+ show();
+ } else {
+ hide();
+ }
+ }
+
+ private boolean isInViewport() {
+ int top = (int) Math.ceil(getTop());
+ int height = (int) Math.floor(getHeight());
+ Range location = Range.withLength(top, height);
+ return getViewportPixels().intersects(location);
+ }
+
+ public void show() {
+ getRootElement().getStyle().clearDisplay();
+ getDecoElement().getStyle().clearDisplay();
+ }
+
+ public void hide() {
+ getRootElement().getStyle().setDisplay(Display.NONE);
+ getDecoElement().getStyle().setDisplay(Display.NONE);
+ }
+
+ /**
+ * Crop the decorator element so that it doesn't overlap the header
+ * and footer sections.
+ *
+ * @param bodyTop
+ * the top cordinate of the escalator body
+ * @param bodyBottom
+ * the bottom cordinate of the escalator body
+ * @param decoWidth
+ * width of the deco
+ */
+ private void updateDecoClip(final double bodyTop,
+ final double bodyBottom, final double decoWidth) {
+ final int top = deco.getAbsoluteTop();
+ final int bottom = deco.getAbsoluteBottom();
+ /*
+ * FIXME
+ *
+ * Height and its use is a workaround for the issue where
+ * coordinates of the deco are not calculated yet. This will
+ * prevent a deco from being displayed when it's added to DOM
+ */
+ final int height = bottom - top;
+ if (top < bodyTop || bottom > bodyBottom) {
+ final double topClip = Math.max(0.0D, bodyTop - top);
+ final double bottomClip = height
+ - Math.max(0.0D, bottom - bodyBottom);
+ // TODO [optimize] not sure how GWT compiles this
+ final String clip = new StringBuilder("rect(")
+ .append(topClip).append("px,").append(decoWidth)
+ .append("px,").append(bottomClip).append("px,0)")
+ .toString();
+ deco.getStyle().setProperty("clip", clip);
+ } else {
+ deco.getStyle().setProperty("clip", "auto");
+ }
+ }
+ }
+
+ private final TreeMap<Integer, SpacerImpl> rowIndexToSpacer = new TreeMap<Integer, SpacerImpl>();
+
+ private SpacerUpdater spacerUpdater = SpacerUpdater.NULL;
+
+ private final ScrollHandler spacerScroller = new ScrollHandler() {
+ private double prevScrollX = 0;
+
+ @Override
+ public void onScroll(ScrollEvent event) {
+ if (WidgetUtil.pixelValuesEqual(getScrollLeft(), prevScrollX)) {
+ return;
+ }
+
+ prevScrollX = getScrollLeft();
+ for (SpacerImpl spacer : rowIndexToSpacer.values()) {
+ spacer.setPosition(prevScrollX, spacer.getTop());
+ }
+ }
+ };
+ private HandlerRegistration spacerScrollerRegistration;
+
+ /** Width of the spacers' decos. Calculated once then cached. */
+ private double spacerDecoWidth = 0.0D;
+
+ public void setSpacer(int rowIndex, double height)
+ throws IllegalArgumentException {
+
+ if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) {
+ throw new IllegalArgumentException("invalid row index: "
+ + rowIndex + ", while the body only has "
+ + getBody().getRowCount() + " rows.");
+ }
+
+ if (height >= 0) {
+ if (!spacerExists(rowIndex)) {
+ insertNewSpacer(rowIndex, height);
+ } else {
+ updateExistingSpacer(rowIndex, height);
+ }
+ } else if (spacerExists(rowIndex)) {
+ removeSpacer(rowIndex);
+ }
+
+ updateSpacerDecosVisibility();
+ }
+
+ /** Checks if a given element is a spacer element */
+ public boolean isSpacer(Element row) {
+
+ /*
+ * If this needs optimization, we could do a more heuristic check
+ * based on stylenames and stuff, instead of iterating through the
+ * map.
+ */
+
+ for (SpacerImpl spacer : rowIndexToSpacer.values()) {
+ if (spacer.getRootElement().equals(row)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @SuppressWarnings("boxing")
+ void scrollToSpacer(int spacerIndex, ScrollDestination destination,
+ int padding) {
+
+ assert !destination.equals(ScrollDestination.MIDDLE)
+ || padding != 0 : "destination/padding check should be done before this method";
+
+ if (!rowIndexToSpacer.containsKey(spacerIndex)) {
+ throw new IllegalArgumentException("No spacer open at index "
+ + spacerIndex);
+ }
+
+ SpacerImpl spacer = rowIndexToSpacer.get(spacerIndex);
+ double targetStartPx = spacer.getTop();
+ double targetEndPx = targetStartPx + spacer.getHeight();
+
+ Range viewportPixels = getViewportPixels();
+ double viewportStartPx = viewportPixels.getStart();
+ double viewportEndPx = viewportPixels.getEnd();
+
+ double scrollTop = getScrollPos(destination, targetStartPx,
+ targetEndPx, viewportStartPx, viewportEndPx, padding);
+
+ setScrollTop(scrollTop);
+ }
+
+ public void reapplySpacerWidths() {
+ // FIXME #16266 , spacers get couple pixels too much because borders
+ final double width = getInnerWidth() - spacerDecoWidth;
+ for (SpacerImpl spacer : rowIndexToSpacer.values()) {
+ spacer.getRootElement().getStyle().setWidth(width, Unit.PX);
+ }
+ }
+
+ public void paintRemoveSpacers(Range removedRowsRange) {
+ removeSpacers(removedRowsRange);
+ shiftSpacersByRows(removedRowsRange.getStart(),
+ -removedRowsRange.length());
+ }
+
+ @SuppressWarnings("boxing")
+ public void removeSpacers(Range removedRange) {
+
+ Map<Integer, SpacerImpl> removedSpacers = rowIndexToSpacer
+ .subMap(removedRange.getStart(), true,
+ removedRange.getEnd(), false);
+
+ if (removedSpacers.isEmpty()) {
+ return;
+ }
+
+ for (SpacerImpl spacer : removedSpacers.values()) {
+ /*
+ * [[optimization]] TODO: Each invocation of the setHeight
+ * method has a cascading effect in the DOM. if this proves to
+ * be slow, the DOM offset could be updated as a batch.
+ */
+
+ destroySpacerContent(spacer);
+ spacer.setHeight(0); // resets row offsets
+ spacer.getRootElement().removeFromParent();
+ spacer.getDecoElement().removeFromParent();
+ }
+
+ removedSpacers.clear();
+
+ if (rowIndexToSpacer.isEmpty()) {
+ assert spacerScrollerRegistration != null : "Spacer scroller registration was null";
+ spacerScrollerRegistration.removeHandler();
+ spacerScrollerRegistration = null;
+ }
+ }
+
+ public Map<Integer, SpacerImpl> getSpacers() {
+ return new HashMap<Integer, SpacerImpl>(rowIndexToSpacer);
+ }
+
+ /**
+ * Calculates the sum of all spacers.
+ *
+ * @return sum of all spacers, or 0 if no spacers present
+ */
+ public double getSpacerHeightsSum() {
+ return getHeights(rowIndexToSpacer.values());
+ }
+
+ /**
+ * Calculates the sum of all spacers from one row index onwards.
+ *
+ * @param logicalRowIndex
+ * the spacer to include as the first calculated spacer
+ * @return the sum of all spacers from {@code logicalRowIndex} and
+ * onwards, or 0 if no suitable spacers were found
+ */
+ @SuppressWarnings("boxing")
+ public Collection<SpacerImpl> getSpacersForRowAndAfter(
+ int logicalRowIndex) {
+ return new ArrayList<SpacerImpl>(rowIndexToSpacer.tailMap(
+ logicalRowIndex, true).values());
+ }
+
+ /**
+ * Get all spacers from one pixel point onwards.
+ * <p>
+ *
+ * In this method, the {@link SpacerInclusionStrategy} has the following
+ * meaning when a spacer lies in the middle of either pixel argument:
+ * <dl>
+ * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE}
+ * <dd>include the spacer
+ * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL}
+ * <dd>include the spacer
+ * <dt>{@link SpacerInclusionStrategy#NONE NONE}
+ * <dd>ignore the spacer
+ * </dl>
+ *
+ * @param px
+ * the pixel point after which to return all spacers
+ * @param strategy
+ * the inclusion strategy regarding the {@code px}
+ * @return a collection of the spacers that exist after {@code px}
+ */
+ public Collection<SpacerImpl> getSpacersAfterPx(final double px,
+ final SpacerInclusionStrategy strategy) {
+
+ ArrayList<SpacerImpl> spacers = new ArrayList<SpacerImpl>(
+ rowIndexToSpacer.values());
+
+ for (int i = 0; i < spacers.size(); i++) {
+ SpacerImpl spacer = spacers.get(i);
+
+ double top = spacer.getTop();
+ double bottom = top + spacer.getHeight();
+
+ if (top > px) {
+ return spacers.subList(i, spacers.size());
+ } else if (bottom > px) {
+ if (strategy == SpacerInclusionStrategy.NONE) {
+ return spacers.subList(i + 1, spacers.size());
+ } else {
+ return spacers.subList(i, spacers.size());
+ }
+ }
+ }
+
+ return Collections.emptySet();
+ }
+
+ /**
+ * Gets the spacers currently rendered in the DOM.
+ *
+ * @return an unmodifiable (but live) collection of the spacers
+ * currently in the DOM
+ */
+ public Collection<SpacerImpl> getSpacersInDom() {
+ return Collections
+ .unmodifiableCollection(rowIndexToSpacer.values());
+ }
+
+ /**
+ * Gets the amount of pixels occupied by spacers between two pixel
+ * points.
+ * <p>
+ * In this method, the {@link SpacerInclusionStrategy} has the following
+ * meaning when a spacer lies in the middle of either pixel argument:
+ * <dl>
+ * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE}
+ * <dd>take the entire spacer into account
+ * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL}
+ * <dd>take only the visible area into account
+ * <dt>{@link SpacerInclusionStrategy#NONE NONE}
+ * <dd>ignore that spacer
+ * </dl>
+ *
+ * @param rangeTop
+ * the top pixel point
+ * @param topInclusion
+ * the inclusion strategy regarding {@code rangeTop}.
+ * @param rangeBottom
+ * the bottom pixel point
+ * @param bottomInclusion
+ * the inclusion strategy regarding {@code rangeBottom}.
+ * @return the pixels occupied by spacers between {@code rangeTop} and
+ * {@code rangeBottom}
+ */
+ public double getSpacerHeightsSumBetweenPx(double rangeTop,
+ SpacerInclusionStrategy topInclusion, double rangeBottom,
+ SpacerInclusionStrategy bottomInclusion) {
+
+ assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom";
+
+ double heights = 0;
+
+ /*
+ * TODO [[optimize]]: this might be somewhat inefficient (due to
+ * iterator-based scanning, instead of using the treemap's search
+ * functionalities). But it should be easy to write, read, verify
+ * and maintain.
+ */
+ for (SpacerImpl spacer : rowIndexToSpacer.values()) {
+ double top = spacer.getTop();
+ double height = spacer.getHeight();
+ double bottom = top + height;
+
+ /*
+ * If we happen to implement a DoubleRange (in addition to the
+ * int-based Range) at some point, the following logic should
+ * probably be converted into using the
+ * Range.partitionWith-equivalent.
+ */
+
+ boolean topIsAboveRange = top < rangeTop;
+ boolean topIsInRange = rangeTop <= top && top <= rangeBottom;
+ boolean topIsBelowRange = rangeBottom < top;
+
+ boolean bottomIsAboveRange = bottom < rangeTop;
+ boolean bottomIsInRange = rangeTop <= bottom
+ && bottom <= rangeBottom;
+ boolean bottomIsBelowRange = rangeBottom < bottom;
+
+ assert topIsAboveRange ^ topIsBelowRange ^ topIsInRange : "Bad top logic";
+ assert bottomIsAboveRange ^ bottomIsBelowRange
+ ^ bottomIsInRange : "Bad bottom logic";
+
+ if (bottomIsAboveRange) {
+ continue;
+ } else if (topIsBelowRange) {
+ return heights;
+ }
+
+ else if (topIsAboveRange && bottomIsInRange) {
+ switch (topInclusion) {
+ case PARTIAL:
+ heights += bottom - rangeTop;
+ break;
+ case COMPLETE:
+ heights += height;
+ break;
+ default:
+ break;
+ }
+ }
+
+ else if (topIsAboveRange && bottomIsBelowRange) {
+
+ /*
+ * Here we arbitrarily decide that the top inclusion will
+ * have the honor of overriding the bottom inclusion if
+ * happens to be a conflict of interests.
+ */
+ switch (topInclusion) {
+ case NONE:
+ return 0;
+ case COMPLETE:
+ return height;
+ case PARTIAL:
+ return rangeBottom - rangeTop;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected inclusion state :" + topInclusion);
+ }
+
+ } else if (topIsInRange && bottomIsInRange) {
+ heights += height;
+ }
+
+ else if (topIsInRange && bottomIsBelowRange) {
+ switch (bottomInclusion) {
+ case PARTIAL:
+ heights += rangeBottom - top;
+ break;
+ case COMPLETE:
+ heights += height;
+ break;
+ default:
+ break;
+ }
+
+ return heights;
+ }
+
+ else {
+ assert false : "Unnaccounted-for situation";
+ }
+ }
+
+ return heights;
+ }
+
+ /**
+ * Gets the amount of pixels occupied by spacers from the top until a
+ * certain spot from the top of the body.
+ *
+ * @param px
+ * pixels counted from the top
+ * @return the pixels occupied by spacers up until {@code px}
+ */
+ public double getSpacerHeightsSumUntilPx(double px) {
+ return getSpacerHeightsSumBetweenPx(0,
+ SpacerInclusionStrategy.PARTIAL, px,
+ SpacerInclusionStrategy.PARTIAL);
+ }
+
+ /**
+ * Gets the amount of pixels occupied by spacers until a logical row
+ * index.
+ *
+ * @param logicalIndex
+ * a logical row index
+ * @return the pixels occupied by spacers up until {@code logicalIndex}
+ */
+ @SuppressWarnings("boxing")
+ public double getSpacerHeightsSumUntilIndex(int logicalIndex) {
+ return getHeights(rowIndexToSpacer.headMap(logicalIndex, false)
+ .values());
+ }
+
+ private double getHeights(Collection<SpacerImpl> spacers) {
+ double heights = 0;
+ for (SpacerImpl spacer : spacers) {
+ heights += spacer.getHeight();
+ }
+ return heights;
+ }
+
+ /**
+ * Gets the height of the spacer for a row index.
+ *
+ * @param rowIndex
+ * the index of the row where the spacer should be
+ * @return the height of the spacer at index {@code rowIndex}, or 0 if
+ * there is no spacer there
+ */
+ public double getSpacerHeight(int rowIndex) {
+ SpacerImpl spacer = getSpacer(rowIndex);
+ if (spacer != null) {
+ return spacer.getHeight();
+ } else {
+ return 0;
+ }
+ }
+
+ private boolean spacerExists(int rowIndex) {
+ return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex));
+ }
+
+ @SuppressWarnings("boxing")
+ private void insertNewSpacer(int rowIndex, double height) {
+
+ if (spacerScrollerRegistration == null) {
+ spacerScrollerRegistration = addScrollHandler(spacerScroller);
+ }
+
+ final SpacerImpl spacer = new SpacerImpl(rowIndex);
+
+ rowIndexToSpacer.put(rowIndex, spacer);
+ // set the position before adding it to DOM
+ positions.set(spacer.getRootElement(), getScrollLeft(),
+ calculateSpacerTop(rowIndex));
+
+ TableRowElement spacerRoot = spacer.getRootElement();
+ spacerRoot.getStyle().setWidth(
+ columnConfiguration.calculateRowWidth(), Unit.PX);
+ body.getElement().appendChild(spacerRoot);
+ spacer.setupDom(height);
+ // set the deco position, requires that spacer is in the DOM
+ positions.set(spacer.getDecoElement(), 0,
+ spacer.getTop() - spacer.getSpacerDecoTopOffset());
+
+ spacerDecoContainer.appendChild(spacer.getDecoElement());
+ if (spacerDecoContainer.getParentElement() == null) {
+ getElement().appendChild(spacerDecoContainer);
+ // calculate the spacer deco width, it won't change
+ spacerDecoWidth = WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(spacer
+ .getDecoElement());
+ }
+
+ initSpacerContent(spacer);
+
+ body.sortDomElements();
+ }
+
+ private void updateExistingSpacer(int rowIndex, double newHeight) {
+ getSpacer(rowIndex).setHeight(newHeight);
+ }
+
+ public SpacerImpl getSpacer(int rowIndex) {
+ return rowIndexToSpacer.get(Integer.valueOf(rowIndex));
+ }
+
+ private void removeSpacer(int rowIndex) {
+ removeSpacers(Range.withOnly(rowIndex));
+ }
+
+ public void setStylePrimaryName(String style) {
+ for (SpacerImpl spacer : rowIndexToSpacer.values()) {
+ spacer.setStylePrimaryName(style);
+ }
+ }
+
+ public void setSpacerUpdater(SpacerUpdater spacerUpdater)
+ throws IllegalArgumentException {
+ if (spacerUpdater == null) {
+ throw new IllegalArgumentException(
+ "spacer updater cannot be null");
+ }
+
+ destroySpacerContent(rowIndexToSpacer.values());
+ this.spacerUpdater = spacerUpdater;
+ initSpacerContent(rowIndexToSpacer.values());
+ }
+
+ public SpacerUpdater getSpacerUpdater() {
+ return spacerUpdater;
+ }
+
+ private void destroySpacerContent(Iterable<SpacerImpl> spacers) {
+ for (SpacerImpl spacer : spacers) {
+ destroySpacerContent(spacer);
+ }
+ }
+
+ private void destroySpacerContent(SpacerImpl spacer) {
+ assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching";
+ assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before detaching";
+ spacerUpdater.destroy(spacer);
+ assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching";
+ assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before detaching";
+ }
+
+ private void initSpacerContent(Iterable<SpacerImpl> spacers) {
+ for (SpacerImpl spacer : spacers) {
+ initSpacerContent(spacer);
+ }
+ }
+
+ private void initSpacerContent(SpacerImpl spacer) {
+ assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before attaching";
+ assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before attaching";
+ spacerUpdater.init(spacer);
+ assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator during attaching";
+ assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator during attaching";
+
+ spacer.updateVisibility();
+ }
+
+ public String getSubPartName(Element subElement) {
+ for (SpacerImpl spacer : rowIndexToSpacer.values()) {
+ if (spacer.getRootElement().isOrHasChild(subElement)) {
+ return "spacer[" + spacer.getRow() + "]";
+ }
+ }
+ return null;
+ }
+
+ public Element getSubPartElement(int index) {
+ SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index));
+ if (spacer != null) {
+ return spacer.getElement();
+ } else {
+ return null;
+ }
+ }
+
+ private double calculateSpacerTop(int logicalIndex) {
+ return body.getRowTop(logicalIndex) + body.getDefaultRowHeight();
+ }
+
+ @SuppressWarnings("boxing")
+ private void shiftSpacerPositionsAfterRow(int changedRowIndex,
+ double diffPx) {
+ for (SpacerImpl spacer : rowIndexToSpacer.tailMap(changedRowIndex,
+ false).values()) {
+ spacer.setPositionDiff(0, diffPx);
+ }
+ }
+
+ /**
+ * Shifts spacers at and after a specific row by an amount of rows.
+ * <p>
+ * This moves both their associated row index and also their visual
+ * placement.
+ * <p>
+ * <em>Note:</em> This method does not check for the validity of any
+ * arguments.
+ *
+ * @param index
+ * the index of first row to move
+ * @param numberOfRows
+ * the number of rows to shift the spacers with. A positive
+ * value is downwards, a negative value is upwards.
+ */
+ public void shiftSpacersByRows(int index, int numberOfRows) {
+ final double pxDiff = numberOfRows * body.getDefaultRowHeight();
+ for (SpacerContainer.SpacerImpl spacer : getSpacersForRowAndAfter(index)) {
+ spacer.setPositionDiff(0, pxDiff);
+ spacer.setRowIndex(spacer.getRow() + numberOfRows);
+ }
+ }
+
+ private void updateSpacerDecosVisibility() {
+ final Range visibleRowRange = getVisibleRowRange();
+ Collection<SpacerImpl> visibleSpacers = rowIndexToSpacer.subMap(
+ visibleRowRange.getStart() - 1,
+ visibleRowRange.getEnd() + 1).values();
+ if (!visibleSpacers.isEmpty()) {
+ final double top = tableWrapper.getAbsoluteTop()
+ + header.getHeightOfSection();
+ final double bottom = tableWrapper.getAbsoluteBottom()
+ - footer.getHeightOfSection();
+ for (SpacerImpl spacer : visibleSpacers) {
+ spacer.updateDecoClip(top, bottom, spacerDecoWidth);
+ }
+ }
+ }
+ }
+
+ private class ElementPositionBookkeeper {
+ /**
+ * A map containing cached values of an element's current top position.
+ */
+ private final Map<Element, Double> elementTopPositionMap = new HashMap<Element, Double>();
+ private final Map<Element, Double> elementLeftPositionMap = new HashMap<Element, Double>();
+
+ public void set(final Element e, final double x, final double y) {
+ assert e != null : "Element was null";
+ position.set(e, x, y);
+ elementTopPositionMap.put(e, Double.valueOf(y));
+ elementLeftPositionMap.put(e, Double.valueOf(x));
+ }
+
+ public double getTop(final Element e) {
+ Double top = elementTopPositionMap.get(e);
+ if (top == null) {
+ throw new IllegalArgumentException("Element " + e
+ + " was not found in the position bookkeeping");
+ }
+ return top.doubleValue();
+ }
+
+ public double getLeft(final Element e) {
+ Double left = elementLeftPositionMap.get(e);
+ if (left == null) {
+ throw new IllegalArgumentException("Element " + e
+ + " was not found in the position bookkeeping");
+ }
+ return left.doubleValue();
+ }
+
+ public void remove(Element e) {
+ elementTopPositionMap.remove(e);
+ elementLeftPositionMap.remove(e);
+ }
+ }
+
+ /**
+ * Utility class for parsing and storing SubPart request string attributes
+ * for Grid and Escalator.
+ *
+ * @since 7.5.0
+ */
+ public static class SubPartArguments {
+ private String type;
+ private int[] indices;
+
+ private SubPartArguments(String type, int[] indices) {
+ /*
+ * The constructor is private so that no third party would by
+ * mistake start using this parsing scheme, since it's not official
+ * by TestBench (yet?).
+ */
+
+ this.type = type;
+ this.indices = indices;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public int getIndicesLength() {
+ return indices.length;
+ }
+
+ public int getIndex(int i) {
+ return indices[i];
+ }
+
+ public int[] getIndices() {
+ return Arrays.copyOf(indices, indices.length);
+ }
+
+ static SubPartArguments create(String subPart) {
+ String[] splitArgs = subPart.split("\\[");
+ String type = splitArgs[0];
+ int[] indices = new int[splitArgs.length - 1];
+ for (int i = 0; i < indices.length; ++i) {
+ String tmp = splitArgs[i + 1];
+ indices[i] = Integer.parseInt(tmp.substring(0,
+ tmp.indexOf("]", 1)));
+ }
+ return new SubPartArguments(type, indices);
+ }
+ }
+
+ // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
+ /**
+ * The solution to
+ * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 30</code>
+ * .
+ * <p>
+ * This constant is placed in the Escalator class, instead of an inner
+ * class, since even mathematical expressions aren't allowed in non-static
+ * inner classes for constants.
+ */
+ private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
+ /**
+ * The solution to
+ * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 40</code>
+ * .
+ * <p>
+ * This constant is placed in the Escalator class, instead of an inner
+ * class, since even mathematical expressions aren't allowed in non-static
+ * inner classes for constants.
+ */
+ private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
+
+ private static final String DEFAULT_WIDTH = "500.0px";
+ private static final String DEFAULT_HEIGHT = "400.0px";
+
+ private FlyweightRow flyweightRow = new FlyweightRow();
+
+ /** The {@code <thead/>} tag. */
+ private final TableSectionElement headElem = TableSectionElement.as(DOM
+ .createTHead());
+ /** The {@code <tbody/>} tag. */
+ private final TableSectionElement bodyElem = TableSectionElement.as(DOM
+ .createTBody());
+ /** The {@code <tfoot/>} tag. */
+ private final TableSectionElement footElem = TableSectionElement.as(DOM
+ .createTFoot());
+
+ /**
+ * TODO: investigate whether this field is now unnecessary, as
+ * {@link ScrollbarBundle} now caches its values.
+ *
+ * @deprecated maybe...
+ */
+ @Deprecated
+ private double tBodyScrollTop = 0;
+
+ /**
+ * TODO: investigate whether this field is now unnecessary, as
+ * {@link ScrollbarBundle} now caches its values.
+ *
+ * @deprecated maybe...
+ */
+ @Deprecated
+ private double tBodyScrollLeft = 0;
+
+ private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
+ private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
+
+ private final HeaderRowContainer header = new HeaderRowContainer(headElem);
+ private final BodyRowContainerImpl body = new BodyRowContainerImpl(bodyElem);
+ private final FooterRowContainer footer = new FooterRowContainer(footElem);
+
+ private final Scroller scroller = new Scroller();
+
+ private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
+ private final DivElement tableWrapper;
+
+ private final DivElement horizontalScrollbarDeco = DivElement.as(DOM
+ .createDiv());
+ private final DivElement headerDeco = DivElement.as(DOM.createDiv());
+ private final DivElement footerDeco = DivElement.as(DOM.createDiv());
+ private final DivElement spacerDecoContainer = DivElement.as(DOM
+ .createDiv());
+
+ private PositionFunction position;
+
+ /** The cached width of the escalator, in pixels. */
+ private double widthOfEscalator = 0;
+ /** The cached height of the escalator, in pixels. */
+ private double heightOfEscalator = 0;
+
+ /** The height of Escalator in terms of body rows. */
+ private double heightByRows = 10.0d;
+
+ /** The height of Escalator, as defined by {@link #setHeight(String)} */
+ private String heightByCss = "";
+
+ private HeightMode heightMode = HeightMode.CSS;
+
+ private boolean layoutIsScheduled = false;
+ private ScheduledCommand layoutCommand = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ recalculateElementSizes();
+ layoutIsScheduled = false;
+ }
+ };
+
+ private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper();
+
+ /**
+ * Creates a new Escalator widget instance.
+ */
+ public Escalator() {
+
+ detectAndApplyPositionFunction();
+ getLogger().info(
+ "Using " + position.getClass().getSimpleName()
+ + " for position");
+
+ final Element root = DOM.createDiv();
+ setElement(root);
+
+ setupScrollbars(root);
+
+ tableWrapper = DivElement.as(DOM.createDiv());
+
+ root.appendChild(tableWrapper);
+
+ final Element table = DOM.createTable();
+ tableWrapper.appendChild(table);
+
+ table.appendChild(headElem);
+ table.appendChild(bodyElem);
+ table.appendChild(footElem);
+
+ Style hCornerStyle = headerDeco.getStyle();
+ hCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(),
+ Unit.PX);
+ hCornerStyle.setDisplay(Display.NONE);
+ root.appendChild(headerDeco);
+
+ Style fCornerStyle = footerDeco.getStyle();
+ fCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(),
+ Unit.PX);
+ fCornerStyle.setDisplay(Display.NONE);
+ root.appendChild(footerDeco);
+
+ Style hWrapperStyle = horizontalScrollbarDeco.getStyle();
+ hWrapperStyle.setDisplay(Display.NONE);
+ hWrapperStyle.setHeight(horizontalScrollbar.getScrollbarThickness(),
+ Unit.PX);
+ root.appendChild(horizontalScrollbarDeco);
+
+ setStylePrimaryName("v-escalator");
+
+ spacerDecoContainer.setAttribute("aria-hidden", "true");
+
+ // init default dimensions
+ setHeight(null);
+ setWidth(null);
+ }
+
+ private void setupScrollbars(final Element root) {
+
+ ScrollHandler scrollHandler = new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ scroller.onScroll();
+ fireEvent(new ScrollEvent());
+ }
+ };
+
+ int scrollbarThickness = WidgetUtil.getNativeScrollbarSize();
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * IE refuses to scroll properly if the DIV isn't at least one pixel
+ * larger than the scrollbar controls themselves. But, probably
+ * because of subpixel rendering, in Grid, one pixel isn't enough,
+ * so we'll add two instead.
+ */
+ if (BrowserInfo.get().isIE9()) {
+ scrollbarThickness += 2;
+ } else {
+ scrollbarThickness += 1;
+ }
+ }
+
+ root.appendChild(verticalScrollbar.getElement());
+ verticalScrollbar.addScrollHandler(scrollHandler);
+ verticalScrollbar.setScrollbarThickness(scrollbarThickness);
+
+ if (BrowserInfo.get().isIE8()) {
+ /*
+ * IE8 will have to compensate for a misalignment where it pops the
+ * scrollbar outside of its box. See Bug 3 in
+ * http://edskes.net/ie/ie8overflowandexpandingboxbugs.htm
+ */
+ Style vScrollStyle = verticalScrollbar.getElement().getStyle();
+ vScrollStyle.setRight(
+ verticalScrollbar.getScrollbarThickness() - 1, Unit.PX);
+ }
+
+ root.appendChild(horizontalScrollbar.getElement());
+ horizontalScrollbar.addScrollHandler(scrollHandler);
+ horizontalScrollbar.setScrollbarThickness(scrollbarThickness);
+ horizontalScrollbar
+ .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() {
+
+ private boolean queued = false;
+
+ @Override
+ public void visibilityChanged(
+ ScrollbarBundle.VisibilityChangeEvent event) {
+ if (queued) {
+ return;
+ }
+ queued = true;
+
+ /*
+ * We either lost or gained a scrollbar. In any case, we
+ * need to change the height, if it's defined by rows.
+ */
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ applyHeightByRows();
+ queued = false;
+ }
+ });
+ }
+ });
+
+ /*
+ * Because of all the IE hacks we've done above, we now have scrollbars
+ * hiding underneath a lot of DOM elements.
+ *
+ * This leads to problems with OSX (and many touch-only devices) when
+ * scrollbars are only shown when scrolling, as the scrollbar elements
+ * are hidden underneath everything. We trust that the scrollbars behave
+ * properly in these situations and simply pop them out with a bit of
+ * z-indexing.
+ */
+ if (WidgetUtil.getNativeScrollbarSize() == 0) {
+ verticalScrollbar.getElement().getStyle().setZIndex(90);
+ horizontalScrollbar.getElement().getStyle().setZIndex(90);
+ }
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ header.autodetectRowHeightLater();
+ body.autodetectRowHeightLater();
+ footer.autodetectRowHeightLater();
+
+ header.paintInsertRows(0, header.getRowCount());
+ footer.paintInsertRows(0, footer.getRowCount());
+
+ // recalculateElementSizes();
+
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ /*
+ * Not a faintest idea why we have to defer this call, but
+ * unless it is deferred, the size of the escalator will be 0x0
+ * after it is first detached and then reattached to the DOM.
+ * This only applies to a bare Escalator; inside a Grid
+ * everything works fine either way.
+ *
+ * The three autodetectRowHeightLater calls above seem obvious
+ * suspects at first. However, they don't seem to have anything
+ * to do with the issue, as they are no-ops in the
+ * detach-reattach case.
+ */
+ recalculateElementSizes();
+ }
+ });
+
+ /*
+ * Note: There's no need to explicitly insert rows into the body.
+ *
+ * recalculateElementSizes will recalculate the height of the body. This
+ * has the side-effect that as the body's size grows bigger (i.e. from 0
+ * to its actual height), more escalator rows are populated. Those
+ * escalator rows are then immediately rendered. This, in effect, is the
+ * same thing as inserting those rows.
+ *
+ * In fact, having an extra paintInsertRows here would lead to duplicate
+ * rows.
+ */
+
+ boolean columnsChanged = false;
+ for (ColumnConfigurationImpl.Column column : columnConfiguration.columns) {
+ boolean columnChanged = column.measureAndSetWidthIfNeeded();
+ if (columnChanged) {
+ columnsChanged = true;
+ }
+ }
+ if (columnsChanged) {
+ header.reapplyColumnWidths();
+ body.reapplyColumnWidths();
+ footer.reapplyColumnWidths();
+ }
+
+ verticalScrollbar.onLoad();
+ horizontalScrollbar.onLoad();
+
+ scroller.attachScrollListener(verticalScrollbar.getElement());
+ scroller.attachScrollListener(horizontalScrollbar.getElement());
+ scroller.attachMousewheelListener(getElement());
+ scroller.attachTouchListeners(getElement());
+ }
+
+ @Override
+ protected void onUnload() {
+
+ scroller.detachScrollListener(verticalScrollbar.getElement());
+ scroller.detachScrollListener(horizontalScrollbar.getElement());
+ scroller.detachMousewheelListener(getElement());
+ scroller.detachTouchListeners(getElement());
+
+ /*
+ * We can call paintRemoveRows here, because static ranges are simple to
+ * remove.
+ */
+ header.paintRemoveRows(0, header.getRowCount());
+ footer.paintRemoveRows(0, footer.getRowCount());
+
+ /*
+ * We can't call body.paintRemoveRows since it relies on rowCount to be
+ * updated correctly. Since it isn't, we'll simply and brutally rip out
+ * the DOM elements (in an elegant way, of course).
+ */
+ int rowsToRemove = body.getDomRowCount();
+ for (int i = 0; i < rowsToRemove; i++) {
+ int index = rowsToRemove - i - 1;
+ TableRowElement tr = bodyElem.getRows().getItem(index);
+ body.paintRemoveRow(tr, index);
+ positions.remove(tr);
+ }
+ body.visualRowOrder.clear();
+ body.setTopRowLogicalIndex(0);
+
+ super.onUnload();
+ }
+
+ private void detectAndApplyPositionFunction() {
+ /*
+ * firefox has a bug in its translate operation, showing white space
+ * when adjusting the scrollbar in BodyRowContainer.paintInsertRows
+ */
+ if (Window.Navigator.getUserAgent().contains("Firefox")) {
+ position = new AbsolutePosition();
+ return;
+ }
+
+ final Style docStyle = Document.get().getBody().getStyle();
+ if (hasProperty(docStyle, "transform")) {
+ if (hasProperty(docStyle, "transformStyle")) {
+ position = new Translate3DPosition();
+ } else {
+ position = new TranslatePosition();
+ }
+ } else if (hasProperty(docStyle, "webkitTransform")) {
+ position = new WebkitTranslate3DPosition();
+ } else {
+ position = new AbsolutePosition();
+ }
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(getClass().getName());
+ }
+
+ private static native boolean hasProperty(Style style, String name)
+ /*-{
+ return style[name] !== undefined;
+ }-*/;
+
+ /**
+ * Check whether there are both columns and any row data (for either
+ * headers, body or footer).
+ *
+ * @return <code>true</code> iff header, body or footer has rows && there
+ * are columns
+ */
+ private boolean hasColumnAndRowData() {
+ return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer
+ .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0;
+ }
+
+ /**
+ * Check whether there are any cells in the DOM.
+ *
+ * @return <code>true</code> iff header, body or footer has any child
+ * elements
+ */
+ private boolean hasSomethingInDom() {
+ return headElem.hasChildNodes() || bodyElem.hasChildNodes()
+ || footElem.hasChildNodes();
+ }
+
+ /**
+ * Returns the row container for the header in this Escalator.
+ *
+ * @return the header. Never <code>null</code>
+ */
+ public RowContainer getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the row container for the body in this Escalator.
+ *
+ * @return the body. Never <code>null</code>
+ */
+ public BodyRowContainer getBody() {
+ return body;
+ }
+
+ /**
+ * Returns the row container for the footer in this Escalator.
+ *
+ * @return the footer. Never <code>null</code>
+ */
+ public RowContainer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Returns the configuration object for the columns in this Escalator.
+ *
+ * @return the configuration object for the columns in this Escalator. Never
+ * <code>null</code>
+ */
+ public ColumnConfiguration getColumnConfiguration() {
+ return columnConfiguration;
+ }
+
+ @Override
+ public void setWidth(final String width) {
+ if (width != null && !width.isEmpty()) {
+ super.setWidth(width);
+ } else {
+ super.setWidth(DEFAULT_WIDTH);
+ }
+
+ recalculateElementSizes();
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * If Escalator is currently not in {@link HeightMode#CSS}, the given value
+ * is remembered, and applied once the mode is applied.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(String height) {
+ /*
+ * TODO remove method once RequiresResize and the Vaadin layoutmanager
+ * listening mechanisms are implemented
+ */
+
+ if (height != null && !height.isEmpty()) {
+ heightByCss = height;
+ } else {
+ heightByCss = DEFAULT_HEIGHT;
+ }
+
+ if (getHeightMode() == HeightMode.CSS) {
+ setHeightInternal(height);
+ }
+ }
+
+ private void setHeightInternal(final String height) {
+ final int escalatorRowsBefore = body.visualRowOrder.size();
+
+ if (height != null && !height.isEmpty()) {
+ super.setHeight(height);
+ } else {
+ super.setHeight(DEFAULT_HEIGHT);
+ }
+
+ recalculateElementSizes();
+
+ if (escalatorRowsBefore != body.visualRowOrder.size()) {
+ fireRowVisibilityChangeEvent();
+ }
+ }
+
+ /**
+ * Returns the vertical scroll offset. Note that this is not necessarily the
+ * same as the {@code scrollTop} attribute in the DOM.
+ *
+ * @return the logical vertical scroll offset
+ */
+ public double getScrollTop() {
+ return verticalScrollbar.getScrollPos();
+ }
+
+ /**
+ * Sets the vertical scroll offset. Note that this will not necessarily
+ * become the same as the {@code scrollTop} attribute in the DOM.
+ *
+ * @param scrollTop
+ * the number of pixels to scroll vertically
+ */
+ public void setScrollTop(final double scrollTop) {
+ verticalScrollbar.setScrollPos(scrollTop);
+ }
+
+ /**
+ * Returns the logical horizontal scroll offset. Note that this is not
+ * necessarily the same as the {@code scrollLeft} attribute in the DOM.
+ *
+ * @return the logical horizontal scroll offset
+ */
+ public double getScrollLeft() {
+ return horizontalScrollbar.getScrollPos();
+ }
+
+ /**
+ * Sets the logical horizontal scroll offset. Note that will not necessarily
+ * become the same as the {@code scrollLeft} attribute in the DOM.
+ *
+ * @param scrollLeft
+ * the number of pixels to scroll horizontally
+ */
+ public void setScrollLeft(final double scrollLeft) {
+ horizontalScrollbar.setScrollPos(scrollLeft);
+ }
+
+ /**
+ * Returns the scroll width for the escalator. Note that this is not
+ * necessary the same as {@code Element.scrollWidth} in the DOM.
+ *
+ * @since 7.5.0
+ * @return the scroll width in pixels
+ */
+ public double getScrollWidth() {
+ return horizontalScrollbar.getScrollSize();
+ }
+
+ /**
+ * Returns the scroll height for the escalator. Note that this is not
+ * necessary the same as {@code Element.scrollHeight} in the DOM.
+ *
+ * @since 7.5.0
+ * @return the scroll height in pixels
+ */
+ public double getScrollHeight() {
+ return verticalScrollbar.getScrollSize();
+ }
+
+ /**
+ * Scrolls the body horizontally so that the column at the given index is
+ * visible and there is at least {@code padding} pixels in the direction of
+ * the given scroll destination.
+ *
+ * @param columnIndex
+ * the index of the column to scroll to
+ * @param destination
+ * where the column should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to column and
+ * the viewport edge.
+ * @throws IndexOutOfBoundsException
+ * if {@code columnIndex} is not a valid index for an existing
+ * column
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero; or if the indicated column is frozen;
+ * or if {@code destination == null}
+ */
+ public void scrollToColumn(final int columnIndex,
+ final ScrollDestination destination, final int padding)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ validateScrollDestination(destination, padding);
+ verifyValidColumnIndex(columnIndex);
+
+ if (columnIndex < columnConfiguration.frozenColumns) {
+ throw new IllegalArgumentException("The given column index "
+ + columnIndex + " is frozen.");
+ }
+
+ scroller.scrollToColumn(columnIndex, destination, padding);
+ }
+
+ private void verifyValidColumnIndex(final int columnIndex)
+ throws IndexOutOfBoundsException {
+ if (columnIndex < 0
+ || columnIndex >= columnConfiguration.getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given column index "
+ + columnIndex + " does not exist.");
+ }
+ }
+
+ /**
+ * Scrolls the body vertically so that the row at the given index is visible
+ * and there is at least {@literal padding} pixels to the given scroll
+ * destination.
+ *
+ * @param rowIndex
+ * the index of the logical row to scroll to
+ * @param destination
+ * where the row should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to row and the
+ * viewport edge.
+ * @throws IndexOutOfBoundsException
+ * if {@code rowIndex} is not a valid index for an existing row
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero; or if {@code destination == null}
+ * @see #scrollToRowAndSpacer(int, ScrollDestination, int)
+ * @see #scrollToSpacer(int, ScrollDestination, int)
+ */
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination, final int padding)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ validateScrollDestination(destination, padding);
+ verifyValidRowIndex(rowIndex);
+ scroller.scrollToRow(rowIndex, destination, padding);
+ }
+ });
+ }
+
+ private void verifyValidRowIndex(final int rowIndex) {
+ if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
+ throw new IndexOutOfBoundsException("The given row index "
+ + rowIndex + " does not exist.");
+ }
+ }
+
+ /**
+ * Scrolls the body vertically so that the spacer at the given row index is
+ * visible and there is at least {@literal padding} pixesl to the given
+ * scroll destination.
+ *
+ * @since 7.5.0
+ * @param spacerIndex
+ * the row index of the spacer to scroll to
+ * @param destination
+ * where the spacer should be aligned visually after scrolling
+ * @param padding
+ * the number of pixels to place between the scrolled-to spacer
+ * and the viewport edge
+ * @throws IllegalArgumentException
+ * if {@code spacerIndex} is not an opened spacer; or if
+ * {@code destination} is {@link ScrollDestination#MIDDLE} and
+ * padding is nonzero; or if {@code destination == null}
+ * @see #scrollToRow(int, ScrollDestination, int)
+ * @see #scrollToRowAndSpacer(int, ScrollDestination, int)
+ */
+ public void scrollToSpacer(final int spacerIndex,
+ ScrollDestination destination, final int padding)
+ throws IllegalArgumentException {
+ validateScrollDestination(destination, padding);
+ body.scrollToSpacer(spacerIndex, destination, padding);
+ }
+
+ /**
+ * Scrolls vertically to a row and the spacer below it.
+ * <p>
+ * If a spacer is not open at that index, this method behaves like
+ * {@link #scrollToRow(int, ScrollDestination, int)}
+ *
+ * @since 7.5.0
+ * @param rowIndex
+ * the index of the logical row to scroll to. -1 takes the
+ * topmost spacer into account as well.
+ * @param destination
+ * where the row should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to row and the
+ * viewport edge.
+ * @see #scrollToRow(int, ScrollDestination, int)
+ * @see #scrollToSpacer(int, ScrollDestination, int)
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and {@code padding} is not zero; or if {@code rowIndex} is
+ * not a valid row index, or -1; or if
+ * {@code destination == null}; or if {@code rowIndex == -1} and
+ * there is no spacer open at that index.
+ */
+ public void scrollToRowAndSpacer(final int rowIndex,
+ final ScrollDestination destination, final int padding)
+ throws IllegalArgumentException {
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ validateScrollDestination(destination, padding);
+ if (rowIndex != -1) {
+ verifyValidRowIndex(rowIndex);
+ }
+
+ // row range
+ final Range rowRange;
+ if (rowIndex != -1) {
+ int rowTop = (int) Math.floor(body.getRowTop(rowIndex));
+ int rowHeight = (int) Math.ceil(body.getDefaultRowHeight());
+ rowRange = Range.withLength(rowTop, rowHeight);
+ } else {
+ rowRange = Range.withLength(0, 0);
+ }
+
+ // get spacer
+ final SpacerContainer.SpacerImpl spacer = body.spacerContainer
+ .getSpacer(rowIndex);
+
+ if (rowIndex == -1 && spacer == null) {
+ throw new IllegalArgumentException(
+ "Cannot scroll to row index "
+ + "-1, as there is no spacer open at that index.");
+ }
+
+ // make into target range
+ final Range targetRange;
+ if (spacer != null) {
+ final int spacerTop = (int) Math.floor(spacer.getTop());
+ final int spacerHeight = (int) Math.ceil(spacer.getHeight());
+ Range spacerRange = Range.withLength(spacerTop,
+ spacerHeight);
+
+ targetRange = rowRange.combineWith(spacerRange);
+ } else {
+ targetRange = rowRange;
+ }
+
+ // get params
+ int targetStart = targetRange.getStart();
+ int targetEnd = targetRange.getEnd();
+ double viewportStart = getScrollTop();
+ double viewportEnd = viewportStart + body.getHeightOfSection();
+
+ double scrollPos = getScrollPos(destination, targetStart,
+ targetEnd, viewportStart, viewportEnd, padding);
+
+ setScrollTop(scrollPos);
+ }
+ });
+ }
+
+ private static void validateScrollDestination(
+ final ScrollDestination destination, final int padding) {
+ if (destination == null) {
+ throw new IllegalArgumentException("Destination cannot be null");
+ }
+
+ if (destination == ScrollDestination.MIDDLE && padding != 0) {
+ throw new IllegalArgumentException(
+ "You cannot have a padding with a MIDDLE destination");
+ }
+ }
+
+ /**
+ * Recalculates the dimensions for all elements that require manual
+ * calculations. Also updates the dimension caches.
+ * <p>
+ * <em>Note:</em> This method has the <strong>side-effect</strong>
+ * automatically makes sure that an appropriate amount of escalator rows are
+ * present. So, if the body area grows, more <strong>escalator rows might be
+ * inserted</strong>. Conversely, if the body area shrinks,
+ * <strong>escalator rows might be removed</strong>.
+ */
+ private void recalculateElementSizes() {
+ if (!isAttached()) {
+ return;
+ }
+
+ Profiler.enter("Escalator.recalculateElementSizes");
+ widthOfEscalator = Math.max(0, WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(getElement()));
+ heightOfEscalator = Math.max(0, WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(getElement()));
+
+ header.recalculateSectionHeight();
+ body.recalculateSectionHeight();
+ footer.recalculateSectionHeight();
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ body.verifyEscalatorCount();
+ body.reapplySpacerWidths();
+ Profiler.leave("Escalator.recalculateElementSizes");
+ }
+
+ /**
+ * Snap deltas of x and y to the major four axes (up, down, left, right)
+ * with a threshold of a number of degrees from those axes.
+ *
+ * @param deltaX
+ * the delta in the x axis
+ * @param deltaY
+ * the delta in the y axis
+ * @param thresholdRatio
+ * the threshold in ratio (0..1) between x and y for when to snap
+ * @return a two-element array: <code>[snappedX, snappedY]</code>
+ */
+ private static double[] snapDeltas(final double deltaX,
+ final double deltaY, final double thresholdRatio) {
+
+ final double[] array = new double[2];
+ if (deltaX != 0 && deltaY != 0) {
+ final double aDeltaX = Math.abs(deltaX);
+ final double aDeltaY = Math.abs(deltaY);
+ final double yRatio = aDeltaY / aDeltaX;
+ final double xRatio = aDeltaX / aDeltaY;
+
+ array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
+ array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
+ } else {
+ array[0] = deltaX;
+ array[1] = deltaY;
+ }
+
+ return array;
+ }
+
+ /**
+ * Adds an event handler that gets notified when the range of visible rows
+ * changes e.g. because of scrolling, row resizing or spacers
+ * appearing/disappearing.
+ *
+ * @param rowVisibilityChangeHandler
+ * the event handler
+ * @return a handler registration for the added handler
+ */
+ public HandlerRegistration addRowVisibilityChangeHandler(
+ RowVisibilityChangeHandler rowVisibilityChangeHandler) {
+ return addHandler(rowVisibilityChangeHandler,
+ RowVisibilityChangeEvent.TYPE);
+ }
+
+ private void fireRowVisibilityChangeEvent() {
+ if (!body.visualRowOrder.isEmpty()) {
+ int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder
+ .getFirst());
+ int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder
+ .getLast()) + 1;
+
+ int visibleRowCount = visibleRangeEnd - visibleRangeStart;
+ fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
+ visibleRowCount));
+ } else {
+ fireEvent(new RowVisibilityChangeEvent(0, 0));
+ }
+ }
+
+ /**
+ * Gets the logical index range of currently visible rows.
+ *
+ * @return logical index range of visible rows
+ */
+ public Range getVisibleRowRange() {
+ if (!body.visualRowOrder.isEmpty()) {
+ return Range.withLength(body.getTopRowLogicalIndex(),
+ body.visualRowOrder.size());
+ } else {
+ return Range.withLength(0, 0);
+ }
+ }
+
+ /**
+ * Returns the widget from a cell node or <code>null</code> if there is no
+ * widget in the cell
+ *
+ * @param cellNode
+ * The cell node
+ */
+ static Widget getWidgetFromCell(Node cellNode) {
+ Node possibleWidgetNode = cellNode.getFirstChild();
+ if (possibleWidgetNode != null
+ && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
+ @SuppressWarnings("deprecation")
+ com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
+ .cast();
+ Widget w = WidgetUtil.findWidget(castElement, null);
+
+ // Ensure findWidget did not traverse past the cell element in the
+ // DOM hierarchy
+ if (cellNode.isOrHasChild(w.getElement())) {
+ return w;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+
+ verticalScrollbar.setStylePrimaryName(style);
+ horizontalScrollbar.setStylePrimaryName(style);
+
+ UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
+ UIObject.setStylePrimaryName(headerDeco, style + "-header-deco");
+ UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco");
+ UIObject.setStylePrimaryName(horizontalScrollbarDeco, style
+ + "-horizontal-scrollbar-deco");
+ UIObject.setStylePrimaryName(spacerDecoContainer, style
+ + "-spacer-deco-container");
+
+ header.setStylePrimaryName(style);
+ body.setStylePrimaryName(style);
+ footer.setStylePrimaryName(style);
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Escalator's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Escalator is currently not in {@link HeightMode#ROW}, the given value
+ * is remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * the number of rows that should be visible in Escalator's body
+ * @throws IllegalArgumentException
+ * if {@code rows} is ≤ 0,
+ * {@link Double#isInifinite(double) infinite} or
+ * {@link Double#isNaN(double) NaN}.
+ * @see #setHeightMode(HeightMode)
+ */
+ public void setHeightByRows(double rows) throws IllegalArgumentException {
+ if (rows <= 0) {
+ throw new IllegalArgumentException(
+ "The number of rows must be a positive number.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "The number of rows must be finite.");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("The number must not be NaN.");
+ }
+
+ heightByRows = rows;
+ applyHeightByRows();
+ }
+
+ /**
+ * Gets the amount of rows in Escalator's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * By default, it is 10.
+ *
+ * @return the amount of rows that are being shown in Escalator's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return heightByRows;
+ }
+
+ /**
+ * Reapplies the row-based height of the Grid, if Grid currently should
+ * define its height that way.
+ */
+ private void applyHeightByRows() {
+ if (heightMode != HeightMode.ROW) {
+ return;
+ }
+
+ double headerHeight = header.getHeightOfSection();
+ double footerHeight = footer.getHeightOfSection();
+ double bodyHeight = body.getDefaultRowHeight() * heightByRows;
+ double scrollbar = horizontalScrollbar.showsScrollHandle() ? horizontalScrollbar
+ .getScrollbarThickness() : 0;
+
+ double totalHeight = headerHeight + bodyHeight + scrollbar
+ + footerHeight;
+ setHeightInternal(totalHeight + "px");
+ }
+
+ /**
+ * Defines the mode in which the Escalator widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Escalator will respect the values
+ * given via {@link #setHeight(String)}, and behave as a traditional Widget.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Escalator will make sure that the
+ * {@link #getBody() body} will display as many rows as
+ * {@link #getHeightByRows()} defines. <em>Note:</em> If headers/footers are
+ * inserted or removed, the widget will resize itself to still display the
+ * required amount of rows in its body. It also takes the horizontal
+ * scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Escalator should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ if (heightMode != this.heightMode) {
+ this.heightMode = heightMode;
+
+ switch (this.heightMode) {
+ case CSS:
+ setHeight(heightByCss);
+ break;
+ case ROW:
+ setHeightByRows(heightByRows);
+ break;
+ default:
+ throw new IllegalStateException("Unimplemented feature "
+ + "- unknown HeightMode: " + this.heightMode);
+ }
+ }
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Escalator is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return heightMode;
+ }
+
+ /**
+ * Returns the {@link RowContainer} which contains the element.
+ *
+ * @param element
+ * the element to check for
+ * @return the container the element is in or <code>null</code> if element
+ * is not present in any container.
+ */
+ public RowContainer findRowContainer(Element element) {
+ if (getHeader().getElement() != element
+ && getHeader().getElement().isOrHasChild(element)) {
+ return getHeader();
+ } else if (getBody().getElement() != element
+ && getBody().getElement().isOrHasChild(element)) {
+ return getBody();
+ } else if (getFooter().getElement() != element
+ && getFooter().getElement().isOrHasChild(element)) {
+ return getFooter();
+ }
+ return null;
+ }
+
+ /**
+ * Sets whether a scroll direction is locked or not.
+ * <p>
+ * If a direction is locked, the escalator will refuse to scroll in that
+ * direction.
+ *
+ * @param direction
+ * the orientation of the scroll to set the lock status
+ * @param locked
+ * <code>true</code> to lock, <code>false</code> to unlock
+ */
+ public void setScrollLocked(ScrollbarBundle.Direction direction,
+ boolean locked) {
+ switch (direction) {
+ case HORIZONTAL:
+ horizontalScrollbar.setLocked(locked);
+ break;
+ case VERTICAL:
+ verticalScrollbar.setLocked(locked);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unexpected value: "
+ + direction);
+ }
+ }
+
+ /**
+ * Checks whether or not an direction is locked for scrolling.
+ *
+ * @param direction
+ * the direction of the scroll of which to check the lock status
+ * @return <code>true</code> iff the direction is locked
+ */
+ public boolean isScrollLocked(ScrollbarBundle.Direction direction) {
+ switch (direction) {
+ case HORIZONTAL:
+ return horizontalScrollbar.isLocked();
+ case VERTICAL:
+ return verticalScrollbar.isLocked();
+ default:
+ throw new UnsupportedOperationException("Unexpected value: "
+ + direction);
+ }
+ }
+
+ /**
+ * Adds a scroll handler to this escalator
+ *
+ * @param handler
+ * the scroll handler to add
+ * @return a handler registration for the registered scroll handler
+ */
+ public HandlerRegistration addScrollHandler(ScrollHandler handler) {
+ return addHandler(handler, ScrollEvent.TYPE);
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return body.domSorter.waiting || verticalScrollbar.isWorkPending()
+ || horizontalScrollbar.isWorkPending() || layoutIsScheduled;
+ }
+
+ @Override
+ public void onResize() {
+ if (isAttached() && !layoutIsScheduled) {
+ layoutIsScheduled = true;
+ Scheduler.get().scheduleFinally(layoutCommand);
+ }
+ }
+
+ /**
+ * Gets the maximum number of body rows that can be visible on the screen at
+ * once.
+ *
+ * @return the maximum capacity
+ */
+ public int getMaxVisibleRowCount() {
+ return body.getMaxEscalatorRowCapacity();
+ }
+
+ /**
+ * Gets the escalator's inner width. This is the entire width in pixels,
+ * without the vertical scrollbar.
+ *
+ * @return escalator's inner width
+ */
+ public double getInnerWidth() {
+ return WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(tableWrapper);
+ }
+
+ /**
+ * Resets all cached pixel sizes and reads new values from the DOM. This
+ * methods should be used e.g. when styles affecting the dimensions of
+ * elements in this escalator have been changed.
+ */
+ public void resetSizesFromDom() {
+ header.autodetectRowHeightNow();
+ body.autodetectRowHeightNow();
+ footer.autodetectRowHeightNow();
+
+ for (int i = 0; i < columnConfiguration.getColumnCount(); i++) {
+ columnConfiguration.setColumnWidth(i,
+ columnConfiguration.getColumnWidth(i));
+ }
+ }
+
+ private Range getViewportPixels() {
+ int from = (int) Math.floor(verticalScrollbar.getScrollPos());
+ int to = (int) body.getHeightOfSection();
+ return Range.withLength(from, to);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
+ SubPartArguments args = SubPartArguments.create(subPart);
+
+ Element tableStructureElement = getSubPartElementTableStructure(args);
+ if (tableStructureElement != null) {
+ return DOM.asOld(tableStructureElement);
+ }
+
+ Element spacerElement = getSubPartElementSpacer(args);
+ if (spacerElement != null) {
+ return DOM.asOld(spacerElement);
+ }
+
+ return null;
+ }
+
+ private Element getSubPartElementTableStructure(SubPartArguments args) {
+
+ String type = args.getType();
+ int[] indices = args.getIndices();
+
+ // Get correct RowContainer for type from Escalator
+ RowContainer container = null;
+ if (type.equalsIgnoreCase("header")) {
+ container = getHeader();
+ } else if (type.equalsIgnoreCase("cell")) {
+ // If wanted row is not visible, we need to scroll there.
+ Range visibleRowRange = getVisibleRowRange();
+ if (indices.length > 0 && !visibleRowRange.contains(indices[0])) {
+ try {
+ scrollToRow(indices[0], ScrollDestination.ANY, 0);
+ } catch (IllegalArgumentException e) {
+ getLogger().log(Level.SEVERE, e.getMessage());
+ }
+ // Scrolling causes a lazy loading event. No element can
+ // currently be retrieved.
+ return null;
+ }
+ container = getBody();
+ } else if (type.equalsIgnoreCase("footer")) {
+ container = getFooter();
+ }
+
+ if (null != container) {
+ if (indices.length == 0) {
+ // No indexing. Just return the wanted container element
+ return container.getElement();
+ } else {
+ try {
+ return getSubPart(container, indices);
+ } catch (Exception e) {
+ getLogger().log(Level.SEVERE, e.getMessage());
+ }
+ }
+ }
+ return null;
+ }
+
+ private Element getSubPart(RowContainer container, int[] indices) {
+ Element targetElement = container.getRowElement(indices[0]);
+
+ // Scroll wanted column to view if able
+ if (indices.length > 1 && targetElement != null) {
+ if (getColumnConfiguration().getFrozenColumnCount() <= indices[1]) {
+ scrollToColumn(indices[1], ScrollDestination.ANY, 0);
+ }
+
+ targetElement = getCellFromRow(TableRowElement.as(targetElement),
+ indices[1]);
+
+ for (int i = 2; i < indices.length && targetElement != null; ++i) {
+ targetElement = (Element) targetElement.getChild(indices[i]);
+ }
+ }
+
+ return targetElement;
+ }
+
+ private static Element getCellFromRow(TableRowElement rowElement, int index) {
+ int childCount = rowElement.getCells().getLength();
+ if (index < 0 || index >= childCount) {
+ return null;
+ }
+
+ TableCellElement currentCell = null;
+ boolean indexInColspan = false;
+ int i = 0;
+
+ while (!indexInColspan) {
+ currentCell = rowElement.getCells().getItem(i);
+
+ // Calculate if this is the cell we are looking for
+ int colSpan = currentCell.getColSpan();
+ indexInColspan = index < colSpan + i;
+
+ // Increment by colspan to skip over hidden cells
+ i += colSpan;
+ }
+ return currentCell;
+ }
+
+ private Element getSubPartElementSpacer(SubPartArguments args) {
+ if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) {
+ return body.spacerContainer.getSubPartElement(args.getIndex(0));
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public String getSubPartName(com.google.gwt.user.client.Element subElement) {
+
+ /*
+ * The spacer check needs to be before table structure check, because
+ * (for now) the table structure will take spacer elements into account
+ * as well, when it shouldn't.
+ */
+
+ String spacer = getSubPartNameSpacer(subElement);
+ if (spacer != null) {
+ return spacer;
+ }
+
+ String tableStructure = getSubPartNameTableStructure(subElement);
+ if (tableStructure != null) {
+ return tableStructure;
+ }
+
+ return null;
+ }
+
+ private String getSubPartNameTableStructure(Element subElement) {
+
+ List<RowContainer> containers = Arrays.asList(getHeader(), getBody(),
+ getFooter());
+ List<String> containerType = Arrays.asList("header", "cell", "footer");
+
+ for (int i = 0; i < containers.size(); ++i) {
+ RowContainer container = containers.get(i);
+ boolean containerRow = (subElement.getTagName().equalsIgnoreCase(
+ "tr") && subElement.getParentElement() == container
+ .getElement());
+ if (containerRow) {
+ /*
+ * Wanted SubPart is row that is a child of containers root to
+ * get indices, we use a cell that is a child of this row
+ */
+ subElement = subElement.getFirstChildElement();
+ }
+
+ Cell cell = container.getCell(subElement);
+ if (cell != null) {
+ // Skip the column index if subElement was a child of root
+ return containerType.get(i) + "[" + cell.getRow()
+ + (containerRow ? "]" : "][" + cell.getColumn() + "]");
+ }
+ }
+ return null;
+ }
+
+ private String getSubPartNameSpacer(Element subElement) {
+ return body.spacerContainer.getSubPartName(subElement);
+ }
+
+ private void logWarning(String message) {
+ getLogger().warning(message);
+ }
+
+ /**
+ * This is an internal method for calculating minimum width for Column
+ * resize.
+ *
+ * @return minimum width for column
+ */
+ double getMinCellWidth(int colIndex) {
+ return columnConfiguration.getMinCellWidth(colIndex);
+ }
+}
--- /dev/null
- refreshRow(rowWithFocus);
+/*
+ * Copyright 2000-2014 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.client.widgets;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.core.shared.GWT;
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+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.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.KeyEvent;
+import com.google.gwt.event.dom.client.MouseEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.touch.client.Point;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasEnabled;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.MenuBar;
+import com.google.gwt.user.client.ui.MenuItem;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.ResizeComposite;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.Focusable;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.renderers.ComplexRenderer;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.renderers.WidgetRenderer;
+import com.vaadin.client.ui.FocusUtil;
+import com.vaadin.client.ui.SubPartAware;
+import com.vaadin.client.ui.dd.DragAndDropHandler;
+import com.vaadin.client.ui.dd.DragAndDropHandler.DragAndDropCallback;
+import com.vaadin.client.ui.dd.DragHandle;
+import com.vaadin.client.ui.dd.DragHandle.DragHandleCallback;
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widget.escalator.ColumnConfiguration;
+import com.vaadin.client.widget.escalator.EscalatorUpdater;
+import com.vaadin.client.widget.escalator.FlyweightCell;
+import com.vaadin.client.widget.escalator.Row;
+import com.vaadin.client.widget.escalator.RowContainer;
+import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent;
+import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
+import com.vaadin.client.widget.escalator.ScrollbarBundle.Direction;
+import com.vaadin.client.widget.escalator.Spacer;
+import com.vaadin.client.widget.escalator.SpacerUpdater;
+import com.vaadin.client.widget.grid.AutoScroller;
+import com.vaadin.client.widget.grid.AutoScroller.AutoScrollerCallback;
+import com.vaadin.client.widget.grid.AutoScroller.ScrollAxis;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.CellStyleGenerator;
+import com.vaadin.client.widget.grid.DataAvailableEvent;
+import com.vaadin.client.widget.grid.DataAvailableHandler;
+import com.vaadin.client.widget.grid.DefaultEditorEventHandler;
+import com.vaadin.client.widget.grid.DetailsGenerator;
+import com.vaadin.client.widget.grid.EditorHandler;
+import com.vaadin.client.widget.grid.EditorHandler.EditorRequest;
+import com.vaadin.client.widget.grid.EventCellReference;
+import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.client.widget.grid.RowReference;
+import com.vaadin.client.widget.grid.RowStyleGenerator;
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler;
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler;
+import com.vaadin.client.widget.grid.events.BodyClickHandler;
+import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyDownHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyPressHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyUpHandler;
+import com.vaadin.client.widget.grid.events.ColumnReorderEvent;
+import com.vaadin.client.widget.grid.events.ColumnReorderHandler;
+import com.vaadin.client.widget.grid.events.ColumnResizeEvent;
+import com.vaadin.client.widget.grid.events.ColumnResizeHandler;
+import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent;
+import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler;
+import com.vaadin.client.widget.grid.events.FooterClickHandler;
+import com.vaadin.client.widget.grid.events.FooterDoubleClickHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyDownHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyPressHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyUpHandler;
+import com.vaadin.client.widget.grid.events.GridClickEvent;
+import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
+import com.vaadin.client.widget.grid.events.GridKeyDownEvent;
+import com.vaadin.client.widget.grid.events.GridKeyPressEvent;
+import com.vaadin.client.widget.grid.events.GridKeyUpEvent;
+import com.vaadin.client.widget.grid.events.HeaderClickHandler;
+import com.vaadin.client.widget.grid.events.HeaderDoubleClickHandler;
+import com.vaadin.client.widget.grid.events.HeaderKeyDownHandler;
+import com.vaadin.client.widget.grid.events.HeaderKeyPressHandler;
+import com.vaadin.client.widget.grid.events.HeaderKeyUpHandler;
+import com.vaadin.client.widget.grid.events.ScrollEvent;
+import com.vaadin.client.widget.grid.events.ScrollHandler;
+import com.vaadin.client.widget.grid.events.SelectAllEvent;
+import com.vaadin.client.widget.grid.events.SelectAllHandler;
+import com.vaadin.client.widget.grid.selection.HasSelectionHandlers;
+import com.vaadin.client.widget.grid.selection.MultiSelectionRenderer;
+import com.vaadin.client.widget.grid.selection.SelectionEvent;
+import com.vaadin.client.widget.grid.selection.SelectionHandler;
+import com.vaadin.client.widget.grid.selection.SelectionModel;
+import com.vaadin.client.widget.grid.selection.SelectionModel.Multi;
+import com.vaadin.client.widget.grid.selection.SelectionModel.Single;
+import com.vaadin.client.widget.grid.selection.SelectionModelMulti;
+import com.vaadin.client.widget.grid.selection.SelectionModelNone;
+import com.vaadin.client.widget.grid.selection.SelectionModelSingle;
+import com.vaadin.client.widget.grid.sort.Sort;
+import com.vaadin.client.widget.grid.sort.SortEvent;
+import com.vaadin.client.widget.grid.sort.SortHandler;
+import com.vaadin.client.widget.grid.sort.SortOrder;
+import com.vaadin.client.widgets.Escalator.AbstractRowContainer;
+import com.vaadin.client.widgets.Escalator.SubPartArguments;
+import com.vaadin.client.widgets.Grid.Editor.State;
+import com.vaadin.client.widgets.Grid.StaticSection.StaticCell;
+import com.vaadin.client.widgets.Grid.StaticSection.StaticRow;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.util.SharedUtil;
+
+/**
+ * A data grid view that supports columns and lazy loading of data rows from a
+ * data source.
+ *
+ * <h1>Columns</h1>
+ * <p>
+ * Each column in Grid is represented by a {@link Column}. Each
+ * {@code GridColumn} has a custom implementation for
+ * {@link Column#getValue(Object)} that gets the row object as an argument, and
+ * returns the value for that particular column, extracted from the row object.
+ * <p>
+ * Each column also has a Renderer. Its function is to take the value that is
+ * given by the {@code GridColumn} and display it to the user. A simple column
+ * might have a {@link com.vaadin.client.renderers.TextRenderer TextRenderer}
+ * that simply takes in a {@code String} and displays it as the cell's content.
+ * A more complex renderer might be
+ * {@link com.vaadin.client.renderers.ProgressBarRenderer ProgressBarRenderer}
+ * that takes in a floating point number, and displays a progress bar instead,
+ * based on the given number.
+ * <p>
+ * <em>See:</em> {@link #addColumn(Column)}, {@link #addColumn(Column, int)} and
+ * {@link #addColumns(Column...)}. <em>Also</em>
+ * {@link Column#setRenderer(Renderer)}.
+ *
+ * <h1>Data Sources</h1>
+ * <p>
+ * Grid gets its data from a {@link DataSource}, providing row objects to Grid
+ * from a user-defined endpoint. It can be either a local in-memory data source
+ * (e.g. {@link com.vaadin.client.widget.grid.datasources.ListDataSource
+ * ListDataSource}) or even a remote one, retrieving data from e.g. a REST API
+ * (see {@link com.vaadin.client.data.AbstractRemoteDataSource
+ * AbstractRemoteDataSource}).
+ *
+ *
+ * @param <T>
+ * The row type of the grid. The row type is the POJO type from where
+ * the data is retrieved into the column cells.
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Grid<T> extends ResizeComposite implements
+ HasSelectionHandlers<T>, SubPartAware, DeferredWorker, Focusable,
+ com.google.gwt.user.client.ui.Focusable, HasWidgets, HasEnabled {
+
+ private static final String STYLE_NAME = "v-grid";
+
+ private static final String SELECT_ALL_CHECKBOX_CLASSNAME = "-select-all-checkbox";
+
+ /**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @since 7.5.0
+ *
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+ public abstract static class StaticSection<ROWTYPE extends StaticSection.StaticRow<?>> {
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ *
+ */
+ public static class StaticCell {
+
+ private Object content = null;
+
+ private int colspan = 1;
+
+ private StaticSection<?> section;
+
+ private GridStaticCellType type = GridStaticCellType.TEXT;
+
+ private String styleName = null;
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ this.content = text;
+ this.type = GridStaticCellType.TEXT;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type " + type);
+ }
+ return (String) content;
+ }
+
+ protected StaticSection<?> getSection() {
+ assert section != null;
+ return section;
+ }
+
+ protected void setSection(StaticSection<?> section) {
+ this.section = section;
+ }
+
+ /**
+ * Returns the amount of columns the cell spans. By default is 1.
+ *
+ * @return The amount of columns the cell spans.
+ */
+ public int getColspan() {
+ return colspan;
+ }
+
+ /**
+ * Sets the amount of columns the cell spans. Must be more or equal
+ * to 1. By default is 1.
+ *
+ * @param colspan
+ * the colspan to set
+ */
+ public void setColspan(int colspan) {
+ if (colspan < 1) {
+ throw new IllegalArgumentException(
+ "Colspan cannot be less than 1");
+ }
+
+ this.colspan = colspan;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the html inside the cell.
+ *
+ * @throws IllegalStateException
+ * if trying to retrive HTML from a cell with a type
+ * other than {@link GridStaticCellType#HTML}.
+ * @return the html content of the cell.
+ */
+ public String getHtml() {
+ if (type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type " + type);
+ }
+ return (String) content;
+ }
+
+ /**
+ * Sets the content of the cell to the provided html. All previous
+ * content is discarded and the cell type is set to
+ * {@link GridStaticCellType#HTML}.
+ *
+ * @param html
+ * The html content of the cell
+ */
+ public void setHtml(String html) {
+ this.content = html;
+ this.type = GridStaticCellType.HTML;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the widget in the cell.
+ *
+ * @throws IllegalStateException
+ * if the cell is not {@link GridStaticCellType#WIDGET}
+ *
+ * @return the widget in the cell
+ */
+ public Widget getWidget() {
+ if (type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Widget from a cell with type " + type);
+ }
+ return (Widget) content;
+ }
+
+ /**
+ * Set widget as the content of the cell. The type of the cell
+ * becomes {@link GridStaticCellType#WIDGET}. All previous content
+ * is discarded.
+ *
+ * @param widget
+ * The widget to add to the cell. Should not be
+ * previously attached anywhere (widget.getParent ==
+ * null).
+ */
+ public void setWidget(Widget widget) {
+ if (this.content == widget) {
+ return;
+ }
+
+ if (this.content instanceof Widget) {
+ // Old widget in the cell, detach it first
+ section.getGrid().detachWidget((Widget) this.content);
+ }
+ this.content = widget;
+ this.type = GridStaticCellType.WIDGET;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the type of the cell.
+ *
+ * @return the type of content the cell contains.
+ */
+ public GridStaticCellType getType() {
+ return type;
+ }
+
+ /**
+ * Returns the custom style name for this cell.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return styleName;
+ }
+
+ /**
+ * Sets a custom style name for this cell.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ section.requestSectionRefresh();
+
+ }
+
+ /**
+ * Called when the cell is detached from the row
+ *
+ * @since 7.6.3
+ */
+ void detach() {
+ if (this.content instanceof Widget) {
+ // Widget in the cell, detach it
+ section.getGrid().detachWidget((Widget) this.content);
+ }
+ }
+ }
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ public abstract static class StaticRow<CELLTYPE extends StaticCell> {
+
+ private Map<Column<?, ?>, CELLTYPE> cells = new HashMap<Column<?, ?>, CELLTYPE>();
+
+ private StaticSection<?> section;
+
+ /**
+ * Map from set of spanned columns to cell meta data.
+ */
+ private Map<Set<Column<?, ?>>, CELLTYPE> cellGroups = new HashMap<Set<Column<?, ?>>, CELLTYPE>();
+
+ /**
+ * A custom style name for the row or null if none is set.
+ */
+ private String styleName = null;
+
+ /**
+ * Returns the cell on given GridColumn. If the column is merged
+ * returned cell is the cell for the whole group.
+ *
+ * @param column
+ * the column in grid
+ * @return the cell on given column, merged cell for merged columns,
+ * null if not found
+ */
+ public CELLTYPE getCell(Column<?, ?> column) {
+ Set<Column<?, ?>> cellGroup = getCellGroupForColumn(column);
+ if (cellGroup != null) {
+ return cellGroups.get(cellGroup);
+ }
+ return cells.get(column);
+ }
+
+ /**
+ * Returns <code>true</code> if this row contains spanned cells.
+ *
+ * @since 7.5.0
+ * @return does this row contain spanned cells
+ */
+ public boolean hasSpannedCells() {
+ return !cellGroups.isEmpty();
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param columns
+ * the columns which header should be merged
+ * @return the remaining visible cell after the merge, or the cell
+ * on first column if all are hidden
+ */
+ public CELLTYPE join(Column<?, ?>... columns) {
+ if (columns.length <= 1) {
+ throw new IllegalArgumentException(
+ "You can't merge less than 2 columns together.");
+ }
+
+ HashSet<Column<?, ?>> columnGroup = new HashSet<Column<?, ?>>();
+ // NOTE: this doesn't care about hidden columns, those are
+ // filtered in calculateColspans()
+ for (Column<?, ?> column : columns) {
+ if (!cells.containsKey(column)) {
+ throw new IllegalArgumentException(
+ "Given column does not exists on row " + column);
+ } else if (getCellGroupForColumn(column) != null) {
+ throw new IllegalStateException(
+ "Column is already in a group.");
+ }
+ columnGroup.add(column);
+ }
+
+ CELLTYPE joinedCell = createCell();
+ cellGroups.put(columnGroup, joinedCell);
+ joinedCell.setSection(getSection());
+
+ calculateColspans();
+
+ return joinedCell;
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge, or the first
+ * cell if all columns are hidden
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ if (cells.length <= 1) {
+ throw new IllegalArgumentException(
+ "You can't merge less than 2 cells together.");
+ }
+
+ Column<?, ?>[] columns = new Column<?, ?>[cells.length];
+
+ int j = 0;
+ for (Column<?, ?> column : this.cells.keySet()) {
+ CELLTYPE cell = this.cells.get(column);
+ if (!this.cells.containsValue(cells[j])) {
+ throw new IllegalArgumentException(
+ "Given cell does not exists on row");
+ } else if (cell.equals(cells[j])) {
+ columns[j++] = column;
+ if (j == cells.length) {
+ break;
+ }
+ }
+ }
+
+ return join(columns);
+ }
+
+ private Set<Column<?, ?>> getCellGroupForColumn(Column<?, ?> column) {
+ for (Set<Column<?, ?>> group : cellGroups.keySet()) {
+ if (group.contains(column)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ void calculateColspans() {
+ // Reset all cells
+ for (CELLTYPE cell : this.cells.values()) {
+ cell.setColspan(1);
+ }
+ // Set colspan for grouped cells
+ for (Set<Column<?, ?>> group : cellGroups.keySet()) {
+ if (!checkMergedCellIsContinuous(group)) {
+ // on error simply break the merged cell
+ cellGroups.get(group).setColspan(1);
+ } else {
+ int colSpan = 0;
+ for (Column<?, ?> column : group) {
+ if (!column.isHidden()) {
+ colSpan++;
+ }
+ }
+ // colspan can't be 0
+ cellGroups.get(group).setColspan(Math.max(1, colSpan));
+ }
+ }
+
+ }
+
+ private boolean checkMergedCellIsContinuous(
+ Set<Column<?, ?>> mergedCell) {
+ // no matter if hidden or not, just check for continuous order
+ final List<Column<?, ?>> columnOrder = new ArrayList<Column<?, ?>>(
+ section.grid.getColumns());
+
+ if (!columnOrder.containsAll(mergedCell)) {
+ return false;
+ }
+
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ if (!mergedCell.contains(columnOrder.get(i))) {
+ continue;
+ }
+
+ for (int j = 1; j < mergedCell.size(); ++j) {
+ if (!mergedCell.contains(columnOrder.get(i + j))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ protected void addCell(Column<?, ?> column) {
+ CELLTYPE cell = createCell();
+ cell.setSection(getSection());
+ cells.put(column, cell);
+ }
+
+ protected void removeCell(Column<?, ?> column) {
+ cells.remove(column);
+ }
+
+ protected abstract CELLTYPE createCell();
+
+ protected StaticSection<?> getSection() {
+ return section;
+ }
+
+ protected void setSection(StaticSection<?> section) {
+ this.section = section;
+ }
+
+ /**
+ * Returns the custom style name for this row.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return styleName;
+ }
+
+ /**
+ * Sets a custom style name for this row.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Called when the row is detached from the grid
+ *
+ * @since 7.6.3
+ */
+ void detach() {
+ // Avoid calling detach twice for a merged cell
+ HashSet<CELLTYPE> cells = new HashSet<CELLTYPE>();
+ for (Column<?, ?> column : getSection().grid.getColumns()) {
+ cells.add(getCell(column));
+ }
+ for (CELLTYPE cell : cells) {
+ cell.detach();
+ }
+ }
+ }
+
+ private Grid<?> grid;
+
+ private List<ROWTYPE> rows = new ArrayList<ROWTYPE>();
+
+ private boolean visible = true;
+
+ /**
+ * Creates and returns a new instance of the row type.
+ *
+ * @return the created row
+ */
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that this section should be re-rendered.
+ * <p>
+ * <b>Note</b> that re-render means calling update() on each cell,
+ * preAttach()/postAttach()/preDetach()/postDetach() is not called as
+ * the cells are not removed from the DOM.
+ */
+ protected abstract void requestSectionRefresh();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ this.visible = visible;
+ requestSectionRefresh();
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return visible;
+ }
+
+ /**
+ * Inserts a new row at the given position. Shifts the row currently at
+ * that position and any subsequent rows down (adds one to their
+ * indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(int)
+ * @see #removeRow(StaticRow)
+ */
+ public ROWTYPE addRowAt(int index) {
+ ROWTYPE row = createRow();
+ row.setSection(this);
+ for (int i = 0; i < getGrid().getColumnCount(); ++i) {
+ row.addCell(grid.getColumn(i));
+ }
+ rows.add(index, row);
+
+ requestSectionRefresh();
+ return row;
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ * @see #appendRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(int)
+ * @see #removeRow(StaticRow)
+ */
+ public ROWTYPE prependRow() {
+ return addRowAt(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ * @see #prependRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(int)
+ * @see #removeRow(StaticRow)
+ */
+ public ROWTYPE appendRow() {
+ return addRowAt(rows.size());
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(StaticRow)
+ */
+ public void removeRow(int index) {
+ ROWTYPE row = rows.remove(index);
+ row.detach();
+ requestSectionRefresh();
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(int)
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Returns the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ * @return the row with the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public ROWTYPE getRow(int index) {
+ try {
+ return rows.get(index);
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException("Row with index " + index
+ + " does not exist");
+ }
+ }
+
+ /**
+ * Returns the number of rows in this section.
+ *
+ * @return the number of rows
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected List<ROWTYPE> getRows() {
+ return rows;
+ }
+
+ protected int getVisibleRowCount() {
+ return isVisible() ? getRowCount() : 0;
+ }
+
+ protected void addColumn(Column<?, ?> column) {
+ for (ROWTYPE row : rows) {
+ row.addCell(column);
+ }
+ }
+
+ protected void removeColumn(Column<?, ?> column) {
+ for (ROWTYPE row : rows) {
+ row.removeCell(column);
+ }
+ }
+
+ protected void setGrid(Grid<?> grid) {
+ this.grid = grid;
+ }
+
+ protected Grid<?> getGrid() {
+ assert grid != null;
+ return grid;
+ }
+
+ protected void updateColSpans() {
+ for (ROWTYPE row : rows) {
+ if (row.hasSpannedCells()) {
+ row.calculateColspans();
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents the header section of a Grid. A header consists of a single
+ * header row containing a header cell for each column. Each cell has a
+ * simple textual caption.
+ */
+ protected static class Header extends StaticSection<HeaderRow> {
+ private HeaderRow defaultRow;
+
+ private boolean markAsDirty = false;
+
+ @Override
+ public void removeRow(int index) {
+ HeaderRow removedRow = getRow(index);
+ super.removeRow(index);
+ if (removedRow == defaultRow) {
+ setDefaultRow(null);
+ }
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special
+ * header row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+ if (row != null && !getRows().contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the container");
+ }
+ if (defaultRow != null) {
+ defaultRow.setDefault(false);
+ }
+ if (row != null) {
+ row.setDefault(true);
+ }
+
+ defaultRow = row;
+ requestSectionRefresh();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow();
+ }
+
+ @Override
+ protected void requestSectionRefresh() {
+ markAsDirty = true;
+
+ /*
+ * Defer the refresh so if we multiple times call refreshSection()
+ * (for example when updating cell values) we only get one actual
+ * refresh in the end.
+ */
+ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (markAsDirty) {
+ markAsDirty = false;
+ getGrid().refreshHeader();
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the events consumed by the header
+ *
+ * @return a collection of BrowserEvents
+ */
+ public Collection<String> getConsumedEvents() {
+ return Arrays.asList(BrowserEvents.TOUCHSTART,
+ BrowserEvents.TOUCHMOVE, BrowserEvents.TOUCHEND,
+ BrowserEvents.TOUCHCANCEL, BrowserEvents.CLICK);
+ }
+
+ @Override
+ protected void addColumn(Column<?, ?> column) {
+ super.addColumn(column);
+
+ // Add default content for new columns.
+ if (defaultRow != null) {
+ column.setDefaultHeaderContent(defaultRow.getCell(column));
+ }
+ }
+ }
+
+ /**
+ * A single row in a grid header section.
+ *
+ */
+ public static class HeaderRow extends StaticSection.StaticRow<HeaderCell> {
+
+ private boolean isDefault = false;
+
+ protected void setDefault(boolean isDefault) {
+ this.isDefault = isDefault;
+ if (isDefault) {
+ for (Column<?, ?> column : getSection().grid.getColumns()) {
+ column.setDefaultHeaderContent(getCell(column));
+ }
+ }
+ }
+
+ public boolean isDefault() {
+ return isDefault;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell();
+ }
+ }
+
+ /**
+ * A single cell in a grid header row. Has a caption and, if it's in a
+ * default row, a drag handle.
+ */
+ public static class HeaderCell extends StaticSection.StaticCell {
+ }
+
+ /**
+ * Represents the footer section of a Grid. The footer is always empty.
+ */
+ protected static class Footer extends StaticSection<FooterRow> {
+ private boolean markAsDirty = false;
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow();
+ }
+
+ @Override
+ protected void requestSectionRefresh() {
+ markAsDirty = true;
+
+ /*
+ * Defer the refresh so if we multiple times call refreshSection()
+ * (for example when updating cell values) we only get one actual
+ * refresh in the end.
+ */
+ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (markAsDirty) {
+ markAsDirty = false;
+ getGrid().refreshFooter();
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * A single cell in a grid Footer row. Has a textual caption.
+ *
+ */
+ public static class FooterCell extends StaticSection.StaticCell {
+ }
+
+ /**
+ * A single row in a grid Footer section.
+ *
+ */
+ public static class FooterRow extends StaticSection.StaticRow<FooterCell> {
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell();
+ }
+ }
+
+ private static class EditorRequestImpl<T> implements EditorRequest<T> {
+
+ /**
+ * A callback interface used to notify the invoker of the editor handler
+ * of completed editor requests.
+ *
+ * @param <T>
+ * the row data type
+ */
+ public static interface RequestCallback<T> {
+ /**
+ * The method that must be called when the request has been
+ * processed correctly.
+ *
+ * @param request
+ * the original request object
+ */
+ public void onSuccess(EditorRequest<T> request);
+
+ /**
+ * The method that must be called when processing the request has
+ * produced an aborting error.
+ *
+ * @param request
+ * the original request object
+ */
+ public void onError(EditorRequest<T> request);
+ }
+
+ private Grid<T> grid;
+ private final int rowIndex;
+ private final int columnIndex;
+ private RequestCallback<T> callback;
+ private boolean completed = false;
+
+ public EditorRequestImpl(Grid<T> grid, int rowIndex, int columnIndex,
+ RequestCallback<T> callback) {
+ this.grid = grid;
+ this.rowIndex = rowIndex;
+ this.columnIndex = columnIndex;
+ this.callback = callback;
+ }
+
+ @Override
+ public int getRowIndex() {
+ return rowIndex;
+ }
+
+ @Override
+ public int getColumnIndex() {
+ return columnIndex;
+ }
+
+ @Override
+ public T getRow() {
+ return grid.getDataSource().getRow(rowIndex);
+ }
+
+ @Override
+ public Grid<T> getGrid() {
+ return grid;
+ }
+
+ @Override
+ public Widget getWidget(Grid.Column<?, T> column) {
+ Widget w = grid.getEditorWidget(column);
+ assert w != null;
+ return w;
+ }
+
+ private void complete(String errorMessage,
+ Collection<Column<?, T>> errorColumns) {
+ if (completed) {
+ throw new IllegalStateException(
+ "An EditorRequest must be completed exactly once");
+ }
+ completed = true;
+
+ if (errorColumns == null) {
+ errorColumns = Collections.emptySet();
+ }
+ grid.getEditor().setEditorError(errorMessage, errorColumns);
+ }
+
+ @Override
+ public void success() {
+ complete(null, null);
+ if (callback != null) {
+ callback.onSuccess(this);
+ }
+ }
+
+ @Override
+ public void failure(String errorMessage,
+ Collection<Grid.Column<?, T>> errorColumns) {
+ complete(errorMessage, errorColumns);
+ if (callback != null) {
+ callback.onError(this);
+ }
+ }
+
+ @Override
+ public boolean isCompleted() {
+ return completed;
+ }
+ }
+
+ /**
+ * A wrapper for native DOM events originating from Grid. In addition to the
+ * native event, contains a {@link CellReference} instance specifying which
+ * cell the event originated from.
+ *
+ * @since 7.6
+ * @param <T>
+ * The row type of the grid
+ */
+ public static class GridEvent<T> {
+ private Event event;
+ private EventCellReference<T> cell;
+
+ protected GridEvent(Event event, EventCellReference<T> cell) {
+ this.event = event;
+ this.cell = cell;
+ }
+
+ /**
+ * Returns the wrapped DOM event.
+ *
+ * @return the DOM event
+ */
+ public Event getDomEvent() {
+ return event;
+ }
+
+ /**
+ * Returns the Grid cell this event originated from.
+ *
+ * @return the event cell
+ */
+ public EventCellReference<T> getCell() {
+ return cell;
+ }
+
+ /**
+ * Returns the Grid instance this event originated from.
+ *
+ * @return the grid
+ */
+ public Grid<T> getGrid() {
+ return cell.getGrid();
+ }
+ }
+
+ /**
+ * A wrapper for native DOM events related to the {@link Editor Grid editor}
+ * .
+ *
+ * @since 7.6
+ * @param <T>
+ * the row type of the grid
+ */
+ public static class EditorDomEvent<T> extends GridEvent<T> {
+
+ private final Widget editorWidget;
+
+ protected EditorDomEvent(Event event, EventCellReference<T> cell,
+ Widget editorWidget) {
+ super(event, cell);
+ this.editorWidget = editorWidget;
+ }
+
+ /**
+ * Returns the editor of the Grid this event originated from.
+ *
+ * @return the related editor instance
+ */
+ public Editor<T> getEditor() {
+ return getGrid().getEditor();
+ }
+
+ /**
+ * Returns the currently focused editor widget.
+ *
+ * @return the focused editor widget or {@code null} if not editable
+ */
+ public Widget getEditorWidget() {
+ return editorWidget;
+ }
+
+ /**
+ * Returns the row index the editor is open at. If the editor is not
+ * open, returns -1.
+ *
+ * @return the index of the edited row or -1 if editor is not open
+ */
+ public int getRowIndex() {
+ return getEditor().rowIndex;
+ }
+
+ /**
+ * Returns the column index the editor was opened at. If the editor is
+ * not open, returns -1.
+ *
+ * @return the column index or -1 if editor is not open
+ */
+ public int getFocusedColumnIndex() {
+ return getEditor().focusedColumnIndex;
+ }
+ }
+
+ /**
+ * An editor UI for Grid rows. A single Grid row at a time can be opened for
+ * editing.
+ *
+ * @since 7.6
+ * @param <T>
+ * the row type of the grid
+ */
+ public static class Editor<T> implements DeferredWorker {
+
+ public static final int KEYCODE_SHOW = KeyCodes.KEY_ENTER;
+ public static final int KEYCODE_HIDE = KeyCodes.KEY_ESCAPE;
+
+ private static final String ERROR_CLASS_NAME = "error";
+ private static final String NOT_EDITABLE_CLASS_NAME = "not-editable";
+
+ ScheduledCommand fieldFocusCommand = new ScheduledCommand() {
+ private int count = 0;
+
+ @Override
+ public void execute() {
+ Element focusedElement = WidgetUtil.getFocusedElement();
+ if (focusedElement == grid.getElement()
+ || focusedElement == Document.get().getBody()
+ || count > 2) {
+ focusColumn(focusedColumnIndex);
+ } else {
+ ++count;
+ Scheduler.get().scheduleDeferred(this);
+ }
+ }
+ };
+
+ /**
+ * A handler for events related to the Grid editor. Responsible for
+ * opening, moving or closing the editor based on the received event.
+ *
+ * @since 7.6
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type of the grid
+ */
+ public interface EventHandler<T> {
+ /**
+ * Handles editor-related events in an appropriate way. Opens,
+ * moves, or closes the editor based on the given event.
+ *
+ * @param event
+ * the received event
+ * @return true if the event was handled and nothing else should be
+ * done, false otherwise
+ */
+ boolean handleEvent(EditorDomEvent<T> event);
+ }
+
+ protected enum State {
+ INACTIVE, ACTIVATING, BINDING, ACTIVE, SAVING
+ }
+
+ private Grid<T> grid;
+ private EditorHandler<T> handler;
+ private EventHandler<T> eventHandler = GWT
+ .create(DefaultEditorEventHandler.class);
+
+ private DivElement editorOverlay = DivElement.as(DOM.createDiv());
+ private DivElement cellWrapper = DivElement.as(DOM.createDiv());
+ private DivElement frozenCellWrapper = DivElement.as(DOM.createDiv());
+
+ private DivElement messageAndButtonsWrapper = DivElement.as(DOM
+ .createDiv());
+
+ private DivElement messageWrapper = DivElement.as(DOM.createDiv());
+ private DivElement buttonsWrapper = DivElement.as(DOM.createDiv());
+
+ // Element which contains the error message for the editor
+ // Should only be added to the DOM when there's a message to show
+ private DivElement message = DivElement.as(DOM.createDiv());
+
+ private Map<Column<?, T>, Widget> columnToWidget = new HashMap<Column<?, T>, Widget>();
+ private List<HandlerRegistration> focusHandlers = new ArrayList<HandlerRegistration>();
+
+ private boolean enabled = false;
+ private State state = State.INACTIVE;
+ private int rowIndex = -1;
+ private int focusedColumnIndex = -1;
+ private String styleName = null;
+
+ private HandlerRegistration hScrollHandler;
+ private HandlerRegistration vScrollHandler;
+
+ private final Button saveButton;
+ private final Button cancelButton;
+
+ private static final int SAVE_TIMEOUT_MS = 5000;
+ private final Timer saveTimeout = new Timer() {
+ @Override
+ public void run() {
+ getLogger().warning(
+ "Editor save action is taking longer than expected ("
+ + SAVE_TIMEOUT_MS + "ms). Does your "
+ + EditorHandler.class.getSimpleName()
+ + " remember to call success() or fail()?");
+ }
+ };
+
+ private final EditorRequestImpl.RequestCallback<T> saveRequestCallback = new EditorRequestImpl.RequestCallback<T>() {
+ @Override
+ public void onSuccess(EditorRequest<T> request) {
+ if (state == State.SAVING) {
+ cleanup();
+ cancel();
+ grid.clearSortOrder();
+ }
+ }
+
+ @Override
+ public void onError(EditorRequest<T> request) {
+ if (state == State.SAVING) {
+ cleanup();
+ }
+ }
+
+ private void cleanup() {
+ state = State.ACTIVE;
+ setButtonsEnabled(true);
+ saveTimeout.cancel();
+ }
+ };
+
+ private static final int BIND_TIMEOUT_MS = 5000;
+ private final Timer bindTimeout = new Timer() {
+ @Override
+ public void run() {
+ getLogger().warning(
+ "Editor bind action is taking longer than expected ("
+ + BIND_TIMEOUT_MS + "ms). Does your "
+ + EditorHandler.class.getSimpleName()
+ + " remember to call success() or fail()?");
+ }
+ };
+
+ private final EditorRequestImpl.RequestCallback<T> bindRequestCallback = new EditorRequestImpl.RequestCallback<T>() {
+ @Override
+ public void onSuccess(EditorRequest<T> request) {
+ if (state == State.BINDING) {
+ state = State.ACTIVE;
+ bindTimeout.cancel();
+
+ rowIndex = request.getRowIndex();
+ focusedColumnIndex = request.getColumnIndex();
+ if (focusedColumnIndex >= 0) {
+ // Update internal focus of Grid
+ grid.focusCell(rowIndex, focusedColumnIndex);
+ }
+
+ showOverlay();
+ }
+ }
+
+ @Override
+ public void onError(EditorRequest<T> request) {
+ if (state == State.BINDING) {
+ if (rowIndex == -1) {
+ doCancel();
+ } else {
+ state = State.ACTIVE;
+ // TODO: Maybe restore focus?
+ }
+ bindTimeout.cancel();
+ }
+ }
+ };
+
+ /** A set of all the columns that display an error flag. */
+ private final Set<Column<?, T>> columnErrors = new HashSet<Grid.Column<?, T>>();
+ private boolean buffered = true;
+
+ /** Original position of editor */
+ private double originalTop;
+ /** Original scroll position of grid when editor was opened */
+ private double originalScrollTop;
+ private RowHandle<T> pinnedRowHandle;
+
+ public Editor() {
+ saveButton = new Button();
+ saveButton.setText(GridConstants.DEFAULT_SAVE_CAPTION);
+ saveButton.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ save();
+ }
+ });
+
+ cancelButton = new Button();
+ cancelButton.setText(GridConstants.DEFAULT_CANCEL_CAPTION);
+ cancelButton.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ cancel();
+ }
+ });
+ }
+
+ public void setEditorError(String errorMessage,
+ Collection<Column<?, T>> errorColumns) {
+
+ if (errorMessage == null) {
+ message.removeFromParent();
+ } else {
+ message.setInnerText(errorMessage);
+ if (message.getParentElement() == null) {
+ messageWrapper.appendChild(message);
+ }
+ }
+ // In unbuffered mode only show message wrapper if there is an error
+ if (!isBuffered()) {
+ setMessageAndButtonsWrapperVisible(errorMessage != null);
+ }
+
+ if (state == State.ACTIVE || state == State.SAVING) {
+ for (Column<?, T> c : grid.getColumns()) {
+ grid.getEditor().setEditorColumnError(c,
+ errorColumns.contains(c));
+ }
+ }
+ }
+
+ public int getRow() {
+ return rowIndex;
+ }
+
+ /**
+ * If a cell of this Grid had focus once this editRow call was
+ * triggered, the editor component at the previously focused column
+ * index will be focused.
+ *
+ * If a Grid cell was not focused prior to calling this method, it will
+ * be equivalent to {@code editRow(rowIndex, -1)}.
+ *
+ * @see #editRow(int, int)
+ */
+ public void editRow(int rowIndex) {
+ // Focus the last focused column in the editor iff grid or its child
+ // was focused before the edit request
+ Cell focusedCell = grid.cellFocusHandler.getFocusedCell();
+ Element focusedElement = WidgetUtil.getFocusedElement();
+ if (focusedCell != null && focusedElement != null
+ && grid.getElement().isOrHasChild(focusedElement)) {
+ editRow(rowIndex, focusedCell.getColumn());
+ } else {
+ editRow(rowIndex, -1);
+ }
+ }
+
+ /**
+ * Opens the editor over the row with the given index and attempts to
+ * focus the editor widget in the given column index. Does not move
+ * focus if the widget is not focusable or if the column index is -1.
+ *
+ * @param rowIndex
+ * the index of the row to be edited
+ * @param columnIndex
+ * the column index of the editor widget that should be
+ * initially focused or -1 to not set focus
+ *
+ * @throws IllegalStateException
+ * if this editor is not enabled
+ * @throws IllegalStateException
+ * if this editor is already in edit mode and in buffered
+ * mode
+ *
+ * @since 7.5
+ */
+ public void editRow(final int rowIndex, final int columnIndex) {
+ if (!enabled) {
+ throw new IllegalStateException(
+ "Cannot edit row: editor is not enabled");
+ }
+
+ if (isWorkPending()) {
+ // Request pending a response, don't move try to start another
+ // request.
+ return;
+ }
+
+ if (state != State.INACTIVE && this.rowIndex != rowIndex) {
+ if (isBuffered()) {
+ throw new IllegalStateException(
+ "Cannot edit row: editor already in edit mode");
+ } else if (!columnErrors.isEmpty()) {
+ // Don't move row if errors are present
+
+ // FIXME: Should attempt bind if error field values have
+ // changed.
+
+ return;
+ }
+ }
+ if (columnIndex >= grid.getVisibleColumns().size()) {
+ throw new IllegalArgumentException("Edited column index "
+ + columnIndex
+ + " was bigger than visible column count.");
+ }
+
+ if (this.rowIndex == rowIndex && focusedColumnIndex == columnIndex) {
+ // NO-OP
+ return;
+ }
+
+ if (this.rowIndex == rowIndex) {
+ if (focusedColumnIndex != columnIndex) {
+ if (columnIndex >= grid.getFrozenColumnCount()) {
+ // Scroll to new focused column.
+ grid.getEscalator().scrollToColumn(columnIndex,
+ ScrollDestination.ANY, 0);
+ }
+
+ focusedColumnIndex = columnIndex;
+ }
+
+ updateHorizontalScrollPosition();
+
+ // Update Grid internal focus and focus widget if possible
+ if (focusedColumnIndex >= 0) {
+ grid.focusCell(rowIndex, focusedColumnIndex);
+ focusColumn(focusedColumnIndex);
+ }
+
+ // No need to request anything from the editor handler.
+ return;
+ }
+ state = State.ACTIVATING;
+
+ final Escalator escalator = grid.getEscalator();
+ if (escalator.getVisibleRowRange().contains(rowIndex)) {
+ show(rowIndex, columnIndex);
+ } else {
+ vScrollHandler = grid.addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ if (escalator.getVisibleRowRange().contains(rowIndex)) {
+ show(rowIndex, columnIndex);
+ vScrollHandler.removeHandler();
+ }
+ }
+ });
+ grid.scrollToRow(rowIndex,
+ isBuffered() ? ScrollDestination.MIDDLE
+ : ScrollDestination.ANY);
+ }
+ }
+
+ /**
+ * Cancels the currently active edit and hides the editor. Any changes
+ * that are not {@link #save() saved} are lost.
+ *
+ * @throws IllegalStateException
+ * if this editor is not enabled
+ * @throws IllegalStateException
+ * if this editor is not in edit mode
+ */
+ public void cancel() {
+ if (!enabled) {
+ throw new IllegalStateException(
+ "Cannot cancel edit: editor is not enabled");
+ }
+ if (state == State.INACTIVE) {
+ throw new IllegalStateException(
+ "Cannot cancel edit: editor is not in edit mode");
+ }
+ handler.cancel(new EditorRequestImpl<T>(grid, rowIndex,
+ focusedColumnIndex, null));
+ doCancel();
+ }
+
+ private void doCancel() {
+ hideOverlay();
+ state = State.INACTIVE;
+ rowIndex = -1;
+ focusedColumnIndex = -1;
+ grid.getEscalator().setScrollLocked(Direction.VERTICAL, false);
+ updateSelectionCheckboxesAsNeeded(true);
+ }
+
+ private void updateSelectionCheckboxesAsNeeded(boolean isEnabled) {
+ // FIXME: This is too much guessing. Define a better way to do this.
+ if (grid.selectionColumn != null
+ && grid.selectionColumn.getRenderer() instanceof MultiSelectionRenderer) {
+ grid.refreshBody();
+ CheckBox checkBox = (CheckBox) grid.getDefaultHeaderRow()
+ .getCell(grid.selectionColumn).getWidget();
+ checkBox.setEnabled(isEnabled);
+ }
+ }
+
+ /**
+ * Saves any unsaved changes to the data source and hides the editor.
+ *
+ * @throws IllegalStateException
+ * if this editor is not enabled
+ * @throws IllegalStateException
+ * if this editor is not in edit mode
+ */
+ public void save() {
+ if (!enabled) {
+ throw new IllegalStateException(
+ "Cannot save: editor is not enabled");
+ }
+ if (state != State.ACTIVE) {
+ throw new IllegalStateException(
+ "Cannot save: editor is not in edit mode");
+ }
+
+ state = State.SAVING;
+ setButtonsEnabled(false);
+ saveTimeout.schedule(SAVE_TIMEOUT_MS);
+ EditorRequest<T> request = new EditorRequestImpl<T>(grid, rowIndex,
+ focusedColumnIndex, saveRequestCallback);
+ handler.save(request);
+ updateSelectionCheckboxesAsNeeded(true);
+ }
+
+ /**
+ * Returns the handler responsible for binding data and editor widgets
+ * to this editor.
+ *
+ * @return the editor handler or null if not set
+ */
+ public EditorHandler<T> getHandler() {
+ return handler;
+ }
+
+ /**
+ * Sets the handler responsible for binding data and editor widgets to
+ * this editor.
+ *
+ * @param rowHandler
+ * the new editor handler
+ *
+ * @throws IllegalStateException
+ * if this editor is currently in edit mode
+ */
+ public void setHandler(EditorHandler<T> rowHandler) {
+ if (state != State.INACTIVE) {
+ throw new IllegalStateException(
+ "Cannot set EditorHandler: editor is currently in edit mode");
+ }
+ handler = rowHandler;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Sets the enabled state of this editor.
+ *
+ * @param enabled
+ * true if enabled, false otherwise
+ *
+ * @throws IllegalStateException
+ * if in edit mode and trying to disable
+ * @throws IllegalStateException
+ * if the editor handler is not set
+ */
+ public void setEnabled(boolean enabled) {
+ if (enabled == false && state != State.INACTIVE) {
+ throw new IllegalStateException(
+ "Cannot disable: editor is in edit mode");
+ } else if (enabled == true && getHandler() == null) {
+ throw new IllegalStateException(
+ "Cannot enable: EditorHandler not set");
+ }
+ this.enabled = enabled;
+ }
+
+ protected void show(int rowIndex, int columnIndex) {
+ if (state == State.ACTIVATING) {
+ state = State.BINDING;
+ bindTimeout.schedule(BIND_TIMEOUT_MS);
+ EditorRequest<T> request = new EditorRequestImpl<T>(grid,
+ rowIndex, columnIndex, bindRequestCallback);
+ handler.bind(request);
+ grid.getEscalator().setScrollLocked(Direction.VERTICAL,
+ isBuffered());
+ updateSelectionCheckboxesAsNeeded(false);
+ }
+ }
+
+ protected void setGrid(final Grid<T> grid) {
+ assert grid != null : "Grid cannot be null";
+ assert this.grid == null : "Can only attach editor to Grid once";
+
+ this.grid = grid;
+ }
+
+ protected State getState() {
+ return state;
+ }
+
+ protected void setState(State state) {
+ this.state = state;
+ }
+
+ /**
+ * Returns the editor widget associated with the given column. If the
+ * editor is not active or the column is not
+ * {@link Grid.Column#isEditable() editable}, returns null.
+ *
+ * @param column
+ * the column
+ * @return the widget if the editor is open and the column is editable,
+ * null otherwise
+ */
+ protected Widget getWidget(Column<?, T> column) {
+ return columnToWidget.get(column);
+ }
+
+ /**
+ * Equivalent to {@code showOverlay()}. The argument is ignored.
+ *
+ * @param unused
+ * ignored argument
+ *
+ * @deprecated As of 7.5, use {@link #showOverlay()} instead.
+ */
+ @Deprecated
+ protected void showOverlay(TableRowElement unused) {
+ showOverlay();
+ }
+
+ /**
+ * Opens the editor overlay over the table row indicated by
+ * {@link #getRow()}.
+ *
+ * @since 7.5
+ */
+ protected void showOverlay() {
+ // Ensure overlay is hidden initially
+ hideOverlay();
+ DivElement gridElement = DivElement.as(grid.getElement());
+
+ TableRowElement tr = grid.getEscalator().getBody()
+ .getRowElement(rowIndex);
+
+ hScrollHandler = grid.addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ updateHorizontalScrollPosition();
+ updateVerticalScrollPosition();
+ }
+ });
+
+ gridElement.appendChild(editorOverlay);
+ editorOverlay.appendChild(frozenCellWrapper);
+ editorOverlay.appendChild(cellWrapper);
+ editorOverlay.appendChild(messageAndButtonsWrapper);
+
+ updateBufferedStyleName();
+
+ int frozenColumns = grid.getVisibleFrozenColumnCount();
+ double frozenColumnsWidth = 0;
+ double cellHeight = 0;
+
+ for (int i = 0; i < tr.getCells().getLength(); i++) {
+ Element cell = createCell(tr.getCells().getItem(i));
+ cellHeight = Math.max(cellHeight, WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(tr
+ .getCells().getItem(i)));
+
+ Column<?, T> column = grid.getVisibleColumn(i);
+
+ if (i < frozenColumns) {
+ frozenCellWrapper.appendChild(cell);
+ frozenColumnsWidth += WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(tr
+ .getCells().getItem(i));
+ } else {
+ cellWrapper.appendChild(cell);
+ }
+
+ if (column.isEditable()) {
+ Widget editor = getHandler().getWidget(column);
+
+ if (editor != null) {
+ columnToWidget.put(column, editor);
+ grid.attachWidget(editor, cell);
+ }
+
+ if (i == focusedColumnIndex) {
+ if (BrowserInfo.get().isIE8()) {
+ Scheduler.get().scheduleDeferred(fieldFocusCommand);
+ } else {
+ focusColumn(focusedColumnIndex);
+ }
+ }
+ } else {
+ cell.addClassName(NOT_EDITABLE_CLASS_NAME);
+ cell.addClassName(tr.getCells().getItem(i).getClassName());
+ // If the focused or frozen stylename is present it should
+ // not be inherited by the editor cell as it is not useful
+ // in the editor and would look broken without additional
+ // style rules. This is a bit of a hack.
+ cell.removeClassName(grid.cellFocusStyleName);
+ cell.removeClassName("frozen");
+
+ if (column == grid.selectionColumn) {
+ // Duplicate selection column CheckBox
+
+ pinnedRowHandle = grid.getDataSource().getHandle(
+ grid.getDataSource().getRow(rowIndex));
+ pinnedRowHandle.pin();
+
+ // We need to duplicate the selection CheckBox for the
+ // editor overlay since the original one is hidden by
+ // the overlay
+ final CheckBox checkBox = GWT.create(CheckBox.class);
+ checkBox.setValue(grid.isSelected(pinnedRowHandle
+ .getRow()));
+ checkBox.sinkEvents(Event.ONCLICK);
+
+ checkBox.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ T row = pinnedRowHandle.getRow();
+ if (grid.isSelected(row)) {
+ grid.deselect(row);
+ } else {
+ grid.select(row);
+ }
+ }
+ });
+ grid.attachWidget(checkBox, cell);
+ columnToWidget.put(column, checkBox);
+
+ // Only enable CheckBox in non-buffered mode
+ checkBox.setEnabled(!isBuffered());
+
+ } else if (!(column.getRenderer() instanceof WidgetRenderer)) {
+ // Copy non-widget content directly
+ cell.setInnerHTML(tr.getCells().getItem(i)
+ .getInnerHTML());
+ }
+ }
+ }
+
+ setBounds(frozenCellWrapper, 0, 0, frozenColumnsWidth, 0);
+ setBounds(cellWrapper, frozenColumnsWidth, 0, tr.getOffsetWidth()
+ - frozenColumnsWidth, cellHeight);
+
+ // Only add these elements once
+ if (!messageAndButtonsWrapper.isOrHasChild(messageWrapper)) {
+ messageAndButtonsWrapper.appendChild(messageWrapper);
+ messageAndButtonsWrapper.appendChild(buttonsWrapper);
+ }
+
+ if (isBuffered()) {
+ grid.attachWidget(saveButton, buttonsWrapper);
+ grid.attachWidget(cancelButton, buttonsWrapper);
+ }
+
+ setMessageAndButtonsWrapperVisible(isBuffered());
+
+ updateHorizontalScrollPosition();
+
+ AbstractRowContainer body = (AbstractRowContainer) grid
+ .getEscalator().getBody();
+ double rowTop = body.getRowTop(tr);
+
+ int bodyTop = body.getElement().getAbsoluteTop();
+ int gridTop = gridElement.getAbsoluteTop();
+ double overlayTop = rowTop + bodyTop - gridTop;
+
+ originalScrollTop = grid.getScrollTop();
+ if (!isBuffered() || buttonsShouldBeRenderedBelow(tr)) {
+ // Default case, editor buttons are below the edited row
+ editorOverlay.getStyle().setTop(overlayTop, Unit.PX);
+ originalTop = overlayTop;
+ editorOverlay.getStyle().clearBottom();
+ } else {
+ // Move message and buttons wrapper on top of cell wrapper if
+ // there is not enough space visible space under and fix the
+ // overlay from the bottom
+ editorOverlay.insertFirst(messageAndButtonsWrapper);
+ int gridHeight = grid.getElement().getOffsetHeight();
+ editorOverlay.getStyle()
+ .setBottom(
+ gridHeight - overlayTop - tr.getOffsetHeight(),
+ Unit.PX);
+ editorOverlay.getStyle().clearTop();
+ }
+
+ // Do not render over the vertical scrollbar
+ editorOverlay.getStyle().setWidth(grid.escalator.getInnerWidth(),
+ Unit.PX);
+ }
+
+ private void focusColumn(int colIndex) {
+ if (colIndex < 0 || colIndex >= grid.getVisibleColumns().size()) {
+ // NO-OP
+ return;
+ }
+
+ Widget editor = getWidget(grid.getVisibleColumn(colIndex));
+ if (editor instanceof Focusable) {
+ ((Focusable) editor).focus();
+ } else if (editor instanceof com.google.gwt.user.client.ui.Focusable) {
+ ((com.google.gwt.user.client.ui.Focusable) editor)
+ .setFocus(true);
+ } else {
+ grid.focus();
+ }
+ }
+
+ private boolean buttonsShouldBeRenderedBelow(TableRowElement tr) {
+ TableSectionElement tfoot = grid.escalator.getFooter().getElement();
+ double tfootPageTop = WidgetUtil.getBoundingClientRect(tfoot)
+ .getTop();
+ double trPageBottom = WidgetUtil.getBoundingClientRect(tr)
+ .getBottom();
+ int messageAndButtonsHeight = messageAndButtonsWrapper
+ .getOffsetHeight();
+ double bottomOfButtons = trPageBottom + messageAndButtonsHeight;
+
+ return bottomOfButtons < tfootPageTop;
+ }
+
+ protected void hideOverlay() {
+ if (editorOverlay.getParentElement() == null) {
+ return;
+ }
+
+ if (pinnedRowHandle != null) {
+ pinnedRowHandle.unpin();
+ pinnedRowHandle = null;
+ }
+
+ for (HandlerRegistration r : focusHandlers) {
+ r.removeHandler();
+ }
+ focusHandlers.clear();
+
+ for (Widget w : columnToWidget.values()) {
+ setParent(w, null);
+ }
+ columnToWidget.clear();
+
+ if (isBuffered()) {
+ grid.detachWidget(saveButton);
+ grid.detachWidget(cancelButton);
+ }
+
+ editorOverlay.removeAllChildren();
+ cellWrapper.removeAllChildren();
+ frozenCellWrapper.removeAllChildren();
+ editorOverlay.removeFromParent();
+
+ hScrollHandler.removeHandler();
+
+ clearEditorColumnErrors();
+ }
+
+ private void updateBufferedStyleName() {
+ if (isBuffered()) {
+ editorOverlay.removeClassName("unbuffered");
+ editorOverlay.addClassName("buffered");
+ } else {
+ editorOverlay.removeClassName("buffered");
+ editorOverlay.addClassName("unbuffered");
+ }
+ }
+
+ protected void setStylePrimaryName(String primaryName) {
+ if (styleName != null) {
+ editorOverlay.removeClassName(styleName);
+
+ cellWrapper.removeClassName(styleName + "-cells");
+ frozenCellWrapper.removeClassName(styleName + "-cells");
+ messageAndButtonsWrapper.removeClassName(styleName + "-footer");
+
+ messageWrapper.removeClassName(styleName + "-message");
+ buttonsWrapper.removeClassName(styleName + "-buttons");
+
+ saveButton.removeStyleName(styleName + "-save");
+ cancelButton.removeStyleName(styleName + "-cancel");
+ }
+ styleName = primaryName + "-editor";
+ editorOverlay.setClassName(styleName);
+
+ cellWrapper.setClassName(styleName + "-cells");
+ frozenCellWrapper.setClassName(styleName + "-cells frozen");
+ messageAndButtonsWrapper.setClassName(styleName + "-footer");
+
+ messageWrapper.setClassName(styleName + "-message");
+ buttonsWrapper.setClassName(styleName + "-buttons");
+
+ saveButton.setStyleName(styleName + "-save");
+ cancelButton.setStyleName(styleName + "-cancel");
+ }
+
+ /**
+ * Creates an editor cell corresponding to the given table cell. The
+ * returned element is empty and has the same dimensions and position as
+ * the table cell.
+ *
+ * @param td
+ * the table cell used as a reference
+ * @return an editor cell corresponding to the given cell
+ */
+ protected Element createCell(TableCellElement td) {
+ DivElement cell = DivElement.as(DOM.createDiv());
+ double width = WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(td);
+ double height = WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(td);
+ setBounds(cell, td.getOffsetLeft(), td.getOffsetTop(), width,
+ height);
+ return cell;
+ }
+
+ private static void setBounds(Element e, double left, double top,
+ double width, double height) {
+ Style style = e.getStyle();
+ style.setLeft(left, Unit.PX);
+ style.setTop(top, Unit.PX);
+ style.setWidth(width, Unit.PX);
+ style.setHeight(height, Unit.PX);
+ }
+
+ private void updateHorizontalScrollPosition() {
+ double scrollLeft = grid.getScrollLeft();
+ cellWrapper.getStyle().setLeft(
+ frozenCellWrapper.getOffsetWidth() - scrollLeft, Unit.PX);
+ }
+
+ /**
+ * Moves the editor overlay on scroll so that it stays on top of the
+ * edited row. This will also snap the editor to top or bottom of the
+ * row container if the edited row is scrolled out of the visible area.
+ */
+ private void updateVerticalScrollPosition() {
+ if (isBuffered()) {
+ return;
+ }
+
+ double newScrollTop = grid.getScrollTop();
+
+ int gridTop = grid.getElement().getAbsoluteTop();
+ int editorHeight = editorOverlay.getOffsetHeight();
+
+ Escalator escalator = grid.getEscalator();
+ TableSectionElement header = escalator.getHeader().getElement();
+ int footerTop = escalator.getFooter().getElement().getAbsoluteTop();
+ int headerBottom = header.getAbsoluteBottom();
+
+ double newTop = originalTop - (newScrollTop - originalScrollTop);
+
+ if (newTop + gridTop < headerBottom) {
+ // Snap editor to top of the row container
+ newTop = header.getOffsetHeight();
+ } else if (newTop + gridTop > footerTop - editorHeight) {
+ // Snap editor to the bottom of the row container
+ newTop = footerTop - editorHeight - gridTop;
+ }
+
+ editorOverlay.getStyle().setTop(newTop, Unit.PX);
+ }
+
+ protected void setGridEnabled(boolean enabled) {
+ // TODO: This should be informed to handler as well so possible
+ // fields can be disabled.
+ setButtonsEnabled(enabled);
+ }
+
+ private void setButtonsEnabled(boolean enabled) {
+ saveButton.setEnabled(enabled);
+ cancelButton.setEnabled(enabled);
+ }
+
+ public void setSaveCaption(String saveCaption)
+ throws IllegalArgumentException {
+ if (saveCaption == null) {
+ throw new IllegalArgumentException(
+ "Save caption cannot be null");
+ }
+ saveButton.setText(saveCaption);
+ }
+
+ public String getSaveCaption() {
+ return saveButton.getText();
+ }
+
+ public void setCancelCaption(String cancelCaption)
+ throws IllegalArgumentException {
+ if (cancelCaption == null) {
+ throw new IllegalArgumentException(
+ "Cancel caption cannot be null");
+ }
+ cancelButton.setText(cancelCaption);
+ }
+
+ public String getCancelCaption() {
+ return cancelButton.getText();
+ }
+
+ public void setEditorColumnError(Column<?, T> column, boolean hasError) {
+ if (state != State.ACTIVE && state != State.SAVING) {
+ throw new IllegalStateException("Cannot set cell error "
+ + "status: editor is neither active nor saving.");
+ }
+
+ if (isEditorColumnError(column) == hasError) {
+ return;
+ }
+
+ Element editorCell = getWidget(column).getElement()
+ .getParentElement();
+ if (hasError) {
+ editorCell.addClassName(ERROR_CLASS_NAME);
+ columnErrors.add(column);
+ } else {
+ editorCell.removeClassName(ERROR_CLASS_NAME);
+ columnErrors.remove(column);
+ }
+ }
+
+ public void clearEditorColumnErrors() {
+
+ /*
+ * editorOverlay has no children if it's not active, effectively
+ * making this loop a NOOP.
+ */
+ Element e = editorOverlay.getFirstChildElement();
+ while (e != null) {
+ e.removeClassName(ERROR_CLASS_NAME);
+ e = e.getNextSiblingElement();
+ }
+
+ columnErrors.clear();
+ }
+
+ public boolean isEditorColumnError(Column<?, T> column) {
+ return columnErrors.contains(column);
+ }
+
+ public void setBuffered(boolean buffered) {
+ this.buffered = buffered;
+ setMessageAndButtonsWrapperVisible(buffered);
+ }
+
+ public boolean isBuffered() {
+ return buffered;
+ }
+
+ private void setMessageAndButtonsWrapperVisible(boolean visible) {
+ if (visible) {
+ messageAndButtonsWrapper.getStyle().clearDisplay();
+ } else {
+ messageAndButtonsWrapper.getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ /**
+ * Sets the event handler for this Editor.
+ *
+ * @since 7.6
+ * @param handler
+ * the new event handler
+ */
+ public void setEventHandler(EventHandler<T> handler) {
+ eventHandler = handler;
+ }
+
+ /**
+ * Returns the event handler of this Editor.
+ *
+ * @since 7.6
+ * @return the current event handler
+ */
+ public EventHandler<T> getEventHandler() {
+ return eventHandler;
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return saveTimeout.isRunning() || bindTimeout.isRunning();
+ }
+
+ protected int getElementColumn(Element e) {
+ int frozenCells = frozenCellWrapper.getChildCount();
+ if (frozenCellWrapper.isOrHasChild(e)) {
+ for (int i = 0; i < frozenCells; ++i) {
+ if (frozenCellWrapper.getChild(i).isOrHasChild(e)) {
+ return i;
+ }
+ }
+ }
+
+ if (cellWrapper.isOrHasChild(e)) {
+ for (int i = 0; i < cellWrapper.getChildCount(); ++i) {
+ if (cellWrapper.getChild(i).isOrHasChild(e)) {
+ return i + frozenCells;
+ }
+ }
+ }
+
+ return -1;
+ }
+ }
+
+ public static abstract class AbstractGridKeyEvent<HANDLER extends AbstractGridKeyEventHandler>
+ extends KeyEvent<HANDLER> {
+
+ private Grid<?> grid;
+ private final Type<HANDLER> associatedType = new Type<HANDLER>(
+ getBrowserEventType(), this);
+ private final CellReference<?> targetCell;
+
+ public AbstractGridKeyEvent(Grid<?> grid, CellReference<?> targetCell) {
+ this.grid = grid;
+ this.targetCell = targetCell;
+ }
+
+ protected abstract String getBrowserEventType();
+
+ /**
+ * Gets the Grid instance for this event.
+ *
+ * @return grid
+ */
+ public Grid<?> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the focused cell for this event.
+ *
+ * @return focused cell
+ */
+ public CellReference<?> getFocusedCell() {
+ return targetCell;
+ }
+
+ @Override
+ protected void dispatch(HANDLER handler) {
+ EventTarget target = getNativeEvent().getEventTarget();
+ if (Element.is(target)
+ && !grid.isElementInChildWidget(Element.as(target))) {
+
+ Section section = Section.FOOTER;
+ final RowContainer container = grid.cellFocusHandler.containerWithFocus;
+ if (container == grid.escalator.getHeader()) {
+ section = Section.HEADER;
+ } else if (container == grid.escalator.getBody()) {
+ section = Section.BODY;
+ }
+
+ doDispatch(handler, section);
+ }
+ }
+
+ protected abstract void doDispatch(HANDLER handler, Section section);
+
+ @Override
+ public Type<HANDLER> getAssociatedType() {
+ return associatedType;
+ }
+ }
+
+ public static abstract class AbstractGridMouseEvent<HANDLER extends AbstractGridMouseEventHandler>
+ extends MouseEvent<HANDLER> {
+
+ private Grid<?> grid;
+ private final CellReference<?> targetCell;
+ private final Type<HANDLER> associatedType = new Type<HANDLER>(
+ getBrowserEventType(), this);
+
+ public AbstractGridMouseEvent(Grid<?> grid, CellReference<?> targetCell) {
+ this.grid = grid;
+ this.targetCell = targetCell;
+ }
+
+ protected abstract String getBrowserEventType();
+
+ /**
+ * Gets the Grid instance for this event.
+ *
+ * @return grid
+ */
+ public Grid<?> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the reference of target cell for this event.
+ *
+ * @return target cell
+ */
+ public CellReference<?> getTargetCell() {
+ return targetCell;
+ }
+
+ @Override
+ protected void dispatch(HANDLER handler) {
+ EventTarget target = getNativeEvent().getEventTarget();
+ if (!Element.is(target)) {
+ // Target is not an element
+ return;
+ }
+
+ Element targetElement = Element.as(target);
+ if (grid.isElementInChildWidget(targetElement)) {
+ // Target is some widget inside of Grid
+ return;
+ }
+
+ final RowContainer container = grid.escalator
+ .findRowContainer(targetElement);
+ if (container == null) {
+ // No container for given element
+ return;
+ }
+
+ Section section = Section.FOOTER;
+ if (container == grid.escalator.getHeader()) {
+ section = Section.HEADER;
+ } else if (container == grid.escalator.getBody()) {
+ section = Section.BODY;
+ }
+
+ doDispatch(handler, section);
+ }
+
+ protected abstract void doDispatch(HANDLER handler, Section section);
+
+ @Override
+ public Type<HANDLER> getAssociatedType() {
+ return associatedType;
+ }
+ }
+
+ private static final String CUSTOM_STYLE_PROPERTY_NAME = "customStyle";
+
+ /**
+ * An initial height that is given to new details rows before rendering the
+ * appropriate widget that we then can be measure
+ *
+ * @see GridSpacerUpdater
+ */
+ private static final double DETAILS_ROW_INITIAL_HEIGHT = 50;
+
+ private EventCellReference<T> eventCell = new EventCellReference<T>(this);
+ private GridKeyDownEvent keyDown = new GridKeyDownEvent(this, eventCell);
+ private GridKeyUpEvent keyUp = new GridKeyUpEvent(this, eventCell);
+ private GridKeyPressEvent keyPress = new GridKeyPressEvent(this, eventCell);
+ private GridClickEvent clickEvent = new GridClickEvent(this, eventCell);
+ private GridDoubleClickEvent doubleClickEvent = new GridDoubleClickEvent(
+ this, eventCell);
+
+ private class CellFocusHandler {
+
+ private RowContainer containerWithFocus = escalator.getBody();
+ private int rowWithFocus = 0;
+ private Range cellFocusRange = Range.withLength(0, 1);
+ private int lastFocusedBodyRow = 0;
+ private int lastFocusedHeaderRow = 0;
+ private int lastFocusedFooterRow = 0;
+ private TableCellElement cellWithFocusStyle = null;
+ private TableRowElement rowWithFocusStyle = null;
+
+ public CellFocusHandler() {
+ sinkEvents(getNavigationEvents());
+ }
+
+ private Cell getFocusedCell() {
+ return new Cell(rowWithFocus, cellFocusRange.getStart(),
+ cellWithFocusStyle);
+ }
+
+ /**
+ * Sets style names for given cell when needed.
+ */
+ public void updateFocusedCellStyle(FlyweightCell cell,
+ RowContainer cellContainer) {
+ int cellRow = cell.getRow();
+ int cellColumn = cell.getColumn();
+ int colSpan = cell.getColSpan();
+ boolean columnHasFocus = Range.withLength(cellColumn, colSpan)
+ .intersects(cellFocusRange);
+
+ if (cellContainer == containerWithFocus) {
+ // Cell is in the current container
+ if (cellRow == rowWithFocus && columnHasFocus) {
+ if (cellWithFocusStyle != cell.getElement()) {
+ // Cell is correct but it does not have focused style
+ if (cellWithFocusStyle != null) {
+ // Remove old focus style
+ setStyleName(cellWithFocusStyle,
+ cellFocusStyleName, false);
+ }
+ cellWithFocusStyle = cell.getElement();
+
+ // Add focus style to correct cell.
+ setStyleName(cellWithFocusStyle, cellFocusStyleName,
+ true);
+ }
+ } else if (cellWithFocusStyle == cell.getElement()) {
+ // Due to escalator reusing cells, a new cell has the same
+ // element but is not the focused cell.
+ setStyleName(cellWithFocusStyle, cellFocusStyleName, false);
+ cellWithFocusStyle = null;
+ }
+ }
+ }
+
+ /**
+ * Sets focus style for the given row if needed.
+ *
+ * @param row
+ * a row object
+ */
+ public void updateFocusedRowStyle(Row row) {
+ if (rowWithFocus == row.getRow()
+ && containerWithFocus == escalator.getBody()) {
+ if (row.getElement() != rowWithFocusStyle) {
+ // Row should have focus style but does not have it.
+ if (rowWithFocusStyle != null) {
+ setStyleName(rowWithFocusStyle, rowFocusStyleName,
+ false);
+ }
+ rowWithFocusStyle = row.getElement();
+ setStyleName(rowWithFocusStyle, rowFocusStyleName, true);
+ }
+ } else if (rowWithFocusStyle == row.getElement()
+ || (containerWithFocus != escalator.getBody() && rowWithFocusStyle != null)) {
+ // Remove focus style.
+ setStyleName(rowWithFocusStyle, rowFocusStyleName, false);
+ rowWithFocusStyle = null;
+ }
+ }
+
+ /**
+ * Sets the currently focused.
+ * <p>
+ * <em>NOTE:</em> the column index is the index in DOM, not the logical
+ * column index which includes hidden columns.
+ *
+ * @param rowIndex
+ * the index of the row having focus
+ * @param columnIndexDOM
+ * the index of the cell having focus
+ * @param container
+ * the row container having focus
+ */
+ private void setCellFocus(int rowIndex, int columnIndexDOM,
+ RowContainer container) {
+ if (rowIndex == rowWithFocus
+ && cellFocusRange.contains(columnIndexDOM)
+ && container == this.containerWithFocus) {
+ return;
+ }
+
+ int oldRow = rowWithFocus;
+ rowWithFocus = rowIndex;
+ Range oldRange = cellFocusRange;
+
+ if (container == escalator.getBody()) {
+ scrollToRow(rowWithFocus);
+ cellFocusRange = Range.withLength(columnIndexDOM, 1);
+ } else {
+ int i = 0;
+ Element cell = container.getRowElement(rowWithFocus)
+ .getFirstChildElement();
+ do {
+ int colSpan = cell
+ .getPropertyInt(FlyweightCell.COLSPAN_ATTR);
+ Range cellRange = Range.withLength(i, colSpan);
+ if (cellRange.contains(columnIndexDOM)) {
+ cellFocusRange = cellRange;
+ break;
+ }
+ cell = cell.getNextSiblingElement();
+ ++i;
+ } while (cell != null);
+ }
+ int columnIndex = getColumns().indexOf(
+ getVisibleColumn(columnIndexDOM));
+ if (columnIndex >= escalator.getColumnConfiguration()
+ .getFrozenColumnCount()) {
+ escalator.scrollToColumn(columnIndexDOM, ScrollDestination.ANY,
+ 10);
+ }
+
+ if (this.containerWithFocus == container) {
+ if (oldRange.equals(cellFocusRange) && oldRow != rowWithFocus) {
+ refreshRow(oldRow);
+ } else {
+ refreshHeader();
+ refreshFooter();
+ }
+ } else {
+ RowContainer oldContainer = this.containerWithFocus;
+ this.containerWithFocus = container;
+
+ if (oldContainer == escalator.getBody()) {
+ lastFocusedBodyRow = oldRow;
+ } else if (oldContainer == escalator.getHeader()) {
+ lastFocusedHeaderRow = oldRow;
+ } else {
+ lastFocusedFooterRow = oldRow;
+ }
+
+ if (!oldRange.equals(cellFocusRange)) {
+ refreshHeader();
+ refreshFooter();
+ if (oldContainer == escalator.getBody()) {
+ oldContainer.refreshRows(oldRow, 1);
+ }
+ } else {
+ oldContainer.refreshRows(oldRow, 1);
+ }
+ }
+ refreshRow(rowWithFocus);
+ }
+
+ /**
+ * Sets focus on a cell.
+ *
+ * <p>
+ * <em>Note</em>: cell focus is not the same as JavaScript's
+ * {@code document.activeElement}.
+ *
+ * @param cell
+ * a cell object
+ */
+ public void setCellFocus(CellReference<T> cell) {
+ setCellFocus(cell.getRowIndex(), cell.getColumnIndexDOM(),
+ escalator.findRowContainer(cell.getElement()));
+ }
+
+ /**
+ * Gets list of events that can be used for cell focusing.
+ *
+ * @return list of navigation related event types
+ */
+ public Collection<String> getNavigationEvents() {
+ return Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.CLICK);
+ }
+
+ /**
+ * Handle events that can move the cell focus.
+ */
+ public void handleNavigationEvent(Event event, CellReference<T> cell) {
+ if (event.getType().equals(BrowserEvents.CLICK)) {
+ setCellFocus(cell);
+ // Grid should have focus when clicked.
+ getElement().focus();
+ } else if (event.getType().equals(BrowserEvents.KEYDOWN)) {
+ int newRow = rowWithFocus;
+ RowContainer newContainer = containerWithFocus;
+ int newColumn = cellFocusRange.getStart();
+
+ switch (event.getKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ ++newRow;
+ break;
+ case KeyCodes.KEY_UP:
+ --newRow;
+ break;
+ case KeyCodes.KEY_RIGHT:
+ if (cellFocusRange.getEnd() >= getVisibleColumns().size()) {
+ return;
+ }
+ newColumn = cellFocusRange.getEnd();
+ break;
+ case KeyCodes.KEY_LEFT:
+ if (newColumn == 0) {
+ return;
+ }
+ --newColumn;
+ break;
+ case KeyCodes.KEY_TAB:
+ if (event.getShiftKey()) {
+ newContainer = getPreviousContainer(containerWithFocus);
+ } else {
+ newContainer = getNextContainer(containerWithFocus);
+ }
+
+ if (newContainer == containerWithFocus) {
+ return;
+ }
+ break;
+ case KeyCodes.KEY_HOME:
+ if (newContainer.getRowCount() > 0) {
+ newRow = 0;
+ }
+ break;
+ case KeyCodes.KEY_END:
+ if (newContainer.getRowCount() > 0) {
+ newRow = newContainer.getRowCount() - 1;
+ }
+ break;
+ case KeyCodes.KEY_PAGEDOWN:
+ case KeyCodes.KEY_PAGEUP:
+ if (newContainer.getRowCount() > 0) {
+ boolean down = event.getKeyCode() == KeyCodes.KEY_PAGEDOWN;
+ // If there is a visible focused cell, scroll by one
+ // page from its position. Otherwise, use the first or
+ // the last visible row as the scroll start position.
+ // This avoids jumping when using both keyboard and the
+ // scroll bar for scrolling.
+ int firstVisible = getFirstVisibleRowIndex();
+ int lastVisible = getLastVisibleRowIndex();
+ if (newRow < firstVisible || newRow > lastVisible) {
+ newRow = down ? lastVisible : firstVisible;
+ }
+ // Scroll by a little less than the visible area to
+ // account for the possibility that the top and the
+ // bottom row are only partially visible.
+ int moveFocusBy = Math.max(1, lastVisible
+ - firstVisible - 1);
+ moveFocusBy *= down ? 1 : -1;
+ newRow += moveFocusBy;
+ newRow = Math.max(0, Math.min(
+ newContainer.getRowCount() - 1, newRow));
+ }
+ break;
+ default:
+ return;
+ }
+
+ if (newContainer != containerWithFocus) {
+ if (newContainer == escalator.getBody()) {
+ newRow = lastFocusedBodyRow;
+ } else if (newContainer == escalator.getHeader()) {
+ newRow = lastFocusedHeaderRow;
+ } else {
+ newRow = lastFocusedFooterRow;
+ }
+ } else if (newRow < 0) {
+ newContainer = getPreviousContainer(newContainer);
+
+ if (newContainer == containerWithFocus) {
+ newRow = 0;
+ } else if (newContainer == escalator.getBody()) {
+ newRow = getLastVisibleRowIndex();
+ } else {
+ newRow = newContainer.getRowCount() - 1;
+ }
+ } else if (newRow >= containerWithFocus.getRowCount()) {
+ newContainer = getNextContainer(newContainer);
+
+ if (newContainer == containerWithFocus) {
+ newRow = containerWithFocus.getRowCount() - 1;
+ } else if (newContainer == escalator.getBody()) {
+ newRow = getFirstVisibleRowIndex();
+ } else {
+ newRow = 0;
+ }
+ }
+
+ if (newContainer.getRowCount() == 0) {
+ /*
+ * There are no rows in the container. Can't change the
+ * focused cell.
+ */
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ setCellFocus(newRow, newColumn, newContainer);
+ }
+
+ }
+
+ private RowContainer getPreviousContainer(RowContainer current) {
+ if (current == escalator.getFooter()) {
+ current = escalator.getBody();
+ } else if (current == escalator.getBody()) {
+ current = escalator.getHeader();
+ } else {
+ return current;
+ }
+
+ if (current.getRowCount() == 0) {
+ return getPreviousContainer(current);
+ }
+ return current;
+ }
+
+ private RowContainer getNextContainer(RowContainer current) {
+ if (current == escalator.getHeader()) {
+ current = escalator.getBody();
+ } else if (current == escalator.getBody()) {
+ current = escalator.getFooter();
+ } else {
+ return current;
+ }
+
+ if (current.getRowCount() == 0) {
+ return getNextContainer(current);
+ }
+ return current;
+ }
+
+ private void refreshRow(int row) {
+ containerWithFocus.refreshRows(row, 1);
+ }
+
+ /**
+ * Offsets the focused cell's range.
+ *
+ * @param offset
+ * offset for fixing focused cell's range
+ */
+ public void offsetRangeBy(int offset) {
+ cellFocusRange = cellFocusRange.offsetBy(offset);
+ }
+
+ /**
+ * Informs {@link CellFocusHandler} that certain range of rows has been
+ * added to the Grid body. {@link CellFocusHandler} will fix indices
+ * accordingly.
+ *
+ * @param added
+ * a range of added rows
+ */
+ public void rowsAddedToBody(Range added) {
+ boolean bodyHasFocus = (containerWithFocus == escalator.getBody());
+ boolean insertionIsAboveFocusedCell = (added.getStart() <= rowWithFocus);
+ if (bodyHasFocus && insertionIsAboveFocusedCell) {
+ rowWithFocus += added.length();
+ rowWithFocus = Math.min(rowWithFocus, escalator.getBody()
+ .getRowCount() - 1);
+ refreshRow(rowWithFocus);
+ }
+ }
+
+ /**
+ * Informs {@link CellFocusHandler} that certain range of rows has been
+ * removed from the Grid body. {@link CellFocusHandler} will fix indices
+ * accordingly.
+ *
+ * @param removed
+ * a range of removed rows
+ */
+ public void rowsRemovedFromBody(Range removed) {
+ if (containerWithFocus != escalator.getBody()) {
+ return;
+ } else if (!removed.contains(rowWithFocus)) {
+ if (removed.getStart() > rowWithFocus) {
+ return;
+ }
+ rowWithFocus = rowWithFocus - removed.length();
+ } else {
+ if (containerWithFocus.getRowCount() > removed.getEnd()) {
+ rowWithFocus = removed.getStart();
+ } else if (removed.getStart() > 0) {
+ rowWithFocus = removed.getStart() - 1;
+ } else {
+ if (escalator.getHeader().getRowCount() > 0) {
+ rowWithFocus = Math.min(lastFocusedHeaderRow, escalator
+ .getHeader().getRowCount() - 1);
+ containerWithFocus = escalator.getHeader();
+ } else if (escalator.getFooter().getRowCount() > 0) {
+ rowWithFocus = Math.min(lastFocusedFooterRow, escalator
+ .getFooter().getRowCount() - 1);
+ containerWithFocus = escalator.getFooter();
+ }
+ }
+ }
+ refreshRow(rowWithFocus);
+ }
+ }
+
+ public final class SelectionColumn extends Column<Boolean, T> {
+
+ private boolean initDone = false;
+ private boolean selected = false;
+ private CheckBox selectAllCheckBox;
+
+ SelectionColumn(final Renderer<Boolean> selectColumnRenderer) {
+ super(selectColumnRenderer);
+ }
+
+ void initDone() {
+ setWidth(-1);
+
+ setEditable(false);
+ setResizable(false);
+
+ initDone = true;
+ }
+
+ @Override
+ protected void setDefaultHeaderContent(HeaderCell selectionCell) {
+ /*
+ * TODO: Currently the select all check box is shown when multi
+ * selection is in use. This might result in malfunctions if no
+ * SelectAllHandlers are present.
+ *
+ * Later on this could be fixed so that it check such handlers
+ * exist.
+ */
+ final SelectionModel.Multi<T> model = (Multi<T>) getSelectionModel();
+
+ if (selectAllCheckBox == null) {
+ selectAllCheckBox = GWT.create(CheckBox.class);
+ selectAllCheckBox.setStylePrimaryName(getStylePrimaryName()
+ + SELECT_ALL_CHECKBOX_CLASSNAME);
+ selectAllCheckBox
+ .addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+
+ @Override
+ public void onValueChange(
+ ValueChangeEvent<Boolean> event) {
+ if (event.getValue()) {
+ fireEvent(new SelectAllEvent<T>(model));
+ selected = true;
+ } else {
+ model.deselectAll();
+ selected = false;
+ }
+ }
+ });
+ selectAllCheckBox.setValue(selected);
+
+ addHeaderClickHandler(new HeaderClickHandler() {
+ @Override
+ public void onClick(GridClickEvent event) {
+ CellReference<?> targetCell = event.getTargetCell();
+ int defaultRowIndex = getHeader().getRows().indexOf(
+ getDefaultHeaderRow());
+
+ if (targetCell.getColumnIndex() == 0
+ && targetCell.getRowIndex() == defaultRowIndex) {
+ selectAllCheckBox.setValue(
+ !selectAllCheckBox.getValue(), true);
+ }
+ }
+ });
+
+ // Select all with space when "select all" cell is active
+ addHeaderKeyUpHandler(new HeaderKeyUpHandler() {
+ @Override
+ public void onKeyUp(GridKeyUpEvent event) {
+ if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE) {
+ return;
+ }
+ HeaderRow targetHeaderRow = getHeader().getRow(
+ event.getFocusedCell().getRowIndex());
+ if (!targetHeaderRow.isDefault()) {
+ return;
+ }
+ if (event.getFocusedCell().getColumn() == SelectionColumn.this) {
+ // Send events to ensure state is updated
+ selectAllCheckBox.setValue(
+ !selectAllCheckBox.getValue(), true);
+ }
+ }
+ });
+ } else {
+ for (HeaderRow row : header.getRows()) {
+ if (row.getCell(this).getType() == GridStaticCellType.WIDGET) {
+ // Detach from old header.
+ row.getCell(this).setText("");
+ }
+ }
+ }
+
+ selectionCell.setWidget(selectAllCheckBox);
+ }
+
+ @Override
+ public Column<Boolean, T> setWidth(double pixels) {
+ if (pixels != getWidth() && initDone) {
+ throw new UnsupportedOperationException("The selection "
+ + "column cannot be modified after init");
+ } else {
+ super.setWidth(pixels);
+ }
+
+ return this;
+ }
+
+ @Override
+ public Boolean getValue(T row) {
+ return Boolean.valueOf(isSelected(row));
+ }
+
+ @Override
+ public Column<Boolean, T> setExpandRatio(int ratio) {
+ throw new UnsupportedOperationException(
+ "can't change the expand ratio of the selection column");
+ }
+
+ @Override
+ public int getExpandRatio() {
+ return 0;
+ }
+
+ @Override
+ public Column<Boolean, T> setMaximumWidth(double pixels) {
+ throw new UnsupportedOperationException(
+ "can't change the maximum width of the selection column");
+ }
+
+ @Override
+ public double getMaximumWidth() {
+ return -1;
+ }
+
+ @Override
+ public Column<Boolean, T> setMinimumWidth(double pixels) {
+ throw new UnsupportedOperationException(
+ "can't change the minimum width of the selection column");
+ }
+
+ @Override
+ public double getMinimumWidth() {
+ return -1;
+ }
+
+ @Override
+ public Column<Boolean, T> setEditable(boolean editable) {
+ if (initDone) {
+ throw new UnsupportedOperationException(
+ "can't set the selection column editable");
+ }
+ super.setEditable(editable);
+ return this;
+ }
+ }
+
+ /**
+ * Helper class for performing sorting through the user interface. Controls
+ * the sort() method, reporting USER as the event originator. This is a
+ * completely internal class, and is, as such, safe to re-name should a more
+ * descriptive name come to mind.
+ */
+ private final class UserSorter {
+
+ private final Timer timer;
+ private boolean scheduledMultisort;
+ private Column<?, T> column;
+
+ private UserSorter() {
+ timer = new Timer() {
+
+ @Override
+ public void run() {
+ UserSorter.this.sort(column, scheduledMultisort);
+ }
+ };
+ }
+
+ /**
+ * Toggle sorting for a cell. If the multisort parameter is set to true,
+ * the cell's sort order is modified as a natural part of a multi-sort
+ * chain. If false, the sorting order is set to ASCENDING for that
+ * cell's column. If that column was already the only sorted column in
+ * the Grid, the sort direction is flipped.
+ *
+ * @param cell
+ * a valid cell reference
+ * @param multisort
+ * whether the sort command should act as a multi-sort stack
+ * or not
+ */
+ public void sort(Column<?, ?> column, boolean multisort) {
+
+ if (!columns.contains(column)) {
+ throw new IllegalArgumentException(
+ "Given column is not a column in this grid. "
+ + column.toString());
+ }
+
+ if (!column.isSortable()) {
+ return;
+ }
+
+ final SortOrder so = getSortOrder(column);
+
+ if (multisort) {
+
+ // If the sort order exists, replace existing value with its
+ // opposite
+ if (so != null) {
+ final int idx = sortOrder.indexOf(so);
+ sortOrder.set(idx, so.getOpposite());
+ } else {
+ // If it doesn't, just add a new sort order to the end of
+ // the list
+ sortOrder.add(new SortOrder(column));
+ }
+
+ } else {
+
+ // Since we're doing single column sorting, first clear the
+ // list. Then, if the sort order existed, add its opposite,
+ // otherwise just add a new sort value
+
+ int items = sortOrder.size();
+ sortOrder.clear();
+ if (so != null && items == 1) {
+ sortOrder.add(so.getOpposite());
+ } else {
+ sortOrder.add(new SortOrder(column));
+ }
+ }
+
+ // sortOrder has been changed; tell the Grid to re-sort itself by
+ // user request.
+ Grid.this.sort(true);
+ }
+
+ /**
+ * Perform a sort after a delay.
+ *
+ * @param delay
+ * delay, in milliseconds
+ */
+ public void sortAfterDelay(int delay, boolean multisort) {
+ column = eventCell.getColumn();
+ scheduledMultisort = multisort;
+ timer.schedule(delay);
+ }
+
+ /**
+ * Check if a delayed sort command has been issued but not yet carried
+ * out.
+ *
+ * @return a boolean value
+ */
+ public boolean isDelayedSortScheduled() {
+ return timer.isRunning();
+ }
+
+ /**
+ * Cancel a scheduled sort.
+ */
+ public void cancelDelayedSort() {
+ timer.cancel();
+ }
+
+ }
+
+ /**
+ * @see Grid#autoColumnWidthsRecalculator
+ */
+ private class AutoColumnWidthsRecalculator {
+ private double lastCalculatedInnerWidth = -1;
+
+ private final ScheduledCommand calculateCommand = new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (!isScheduled) {
+ // something cancelled running this.
+ return;
+ }
+
+ if (header.markAsDirty || footer.markAsDirty) {
+ if (rescheduleCount < 10) {
+ /*
+ * Headers and footers are rendered as finally, this way
+ * we re-schedule this loop as finally, at the end of
+ * the queue, so that the headers have a chance to
+ * render themselves.
+ */
+ Scheduler.get().scheduleFinally(this);
+ rescheduleCount++;
+ } else {
+ /*
+ * We've tried too many times reschedule finally. Seems
+ * like something is being deferred. Let the queue
+ * execute and retry again.
+ */
+ rescheduleCount = 0;
+ Scheduler.get().scheduleDeferred(this);
+ }
+ } else if (dataIsBeingFetched) {
+ Scheduler.get().scheduleDeferred(this);
+ } else {
+ calculate();
+ }
+ }
+ };
+
+ private int rescheduleCount = 0;
+ private boolean isScheduled;
+
+ /**
+ * Calculates and applies column widths, taking into account fixed
+ * widths and column expand rules
+ *
+ * @param immediately
+ * <code>true</code> if the widths should be executed
+ * immediately (ignoring lazy loading completely), or
+ * <code>false</code> if the command should be run after a
+ * while (duplicate non-immediately invocations are ignored).
+ * @see Column#setWidth(double)
+ * @see Column#setExpandRatio(int)
+ * @see Column#setMinimumWidth(double)
+ * @see Column#setMaximumWidth(double)
+ */
+ public void schedule() {
+ if (!isScheduled && isAttached()) {
+ isScheduled = true;
+ Scheduler.get().scheduleFinally(calculateCommand);
+ }
+ }
+
+ private void calculate() {
+ isScheduled = false;
+ rescheduleCount = 0;
+
+ assert !dataIsBeingFetched : "Trying to calculate column widths even though data is still being fetched.";
+
+ if (columnsAreGuaranteedToBeWiderThanGrid()) {
+ applyColumnWidths();
+ } else {
+ applyColumnWidthsWithExpansion();
+ }
+
+ // Update latest width to prevent recalculate on height change.
+ lastCalculatedInnerWidth = escalator.getInnerWidth();
+ }
+
+ private boolean columnsAreGuaranteedToBeWiderThanGrid() {
+ double freeSpace = escalator.getInnerWidth();
+ for (Column<?, ?> column : getVisibleColumns()) {
+ if (column.getWidth() >= 0) {
+ freeSpace -= column.getWidth();
+ } else if (column.getMinimumWidth() >= 0) {
+ freeSpace -= column.getMinimumWidth();
+ }
+ }
+ return freeSpace < 0;
+ }
+
+ @SuppressWarnings("boxing")
+ private void applyColumnWidths() {
+
+ /* Step 1: Apply all column widths as they are. */
+
+ Map<Integer, Double> selfWidths = new LinkedHashMap<Integer, Double>();
+ List<Column<?, T>> columns = getVisibleColumns();
+ for (int index = 0; index < columns.size(); index++) {
+ selfWidths.put(index, columns.get(index).getWidth());
+ }
+ Grid.this.escalator.getColumnConfiguration().setColumnWidths(
+ selfWidths);
+
+ /*
+ * Step 2: Make sure that each column ends up obeying their min/max
+ * width constraints if defined as autowidth. If constraints are
+ * violated, fix it.
+ */
+
+ Map<Integer, Double> constrainedWidths = new LinkedHashMap<Integer, Double>();
+ for (int index = 0; index < columns.size(); index++) {
+ Column<?, T> column = columns.get(index);
+
+ boolean hasAutoWidth = column.getWidth() < 0;
+ if (!hasAutoWidth) {
+ continue;
+ }
+
+ // TODO: bug: these don't honor the CSS max/min. :(
+ double actualWidth = column.getWidthActual();
+ if (actualWidth < getMinWidth(column)) {
+ constrainedWidths.put(index, column.getMinimumWidth());
+ } else if (actualWidth > getMaxWidth(column)) {
+ constrainedWidths.put(index, column.getMaximumWidth());
+ }
+ }
+ Grid.this.escalator.getColumnConfiguration().setColumnWidths(
+ constrainedWidths);
+ }
+
+ private void applyColumnWidthsWithExpansion() {
+ boolean defaultExpandRatios = true;
+ int totalRatios = 0;
+ double reservedPixels = 0;
+ final Set<Column<?, T>> columnsToExpand = new HashSet<Column<?, T>>();
+ List<Column<?, T>> nonFixedColumns = new ArrayList<Column<?, T>>();
+ Map<Integer, Double> columnSizes = new HashMap<Integer, Double>();
+ final List<Column<?, T>> visibleColumns = getVisibleColumns();
+
+ /*
+ * Set all fixed widths and also calculate the size-to-fit widths
+ * for the autocalculated columns.
+ *
+ * This way we know with how many pixels we have left to expand the
+ * rest.
+ */
+ for (Column<?, T> column : visibleColumns) {
+ final double widthAsIs = column.getWidth();
+ final boolean isFixedWidth = widthAsIs >= 0;
+ // Check for max width just to be sure we don't break the limits
+ final double widthFixed = Math.max(
+ Math.min(getMaxWidth(column), widthAsIs),
+ column.getMinimumWidth());
+ defaultExpandRatios = defaultExpandRatios
+ && (column.getExpandRatio() == -1 || column == selectionColumn);
+
+ if (isFixedWidth) {
+ columnSizes.put(visibleColumns.indexOf(column), widthFixed);
+ reservedPixels += widthFixed;
+ } else {
+ nonFixedColumns.add(column);
+ columnSizes.put(visibleColumns.indexOf(column), -1.0d);
+ }
+ }
+
+ setColumnSizes(columnSizes);
+
+ for (Column<?, T> column : nonFixedColumns) {
+ final int expandRatio = (defaultExpandRatios ? 1 : column
+ .getExpandRatio());
+ final double maxWidth = getMaxWidth(column);
+ final double newWidth = Math.min(maxWidth,
+ column.getWidthActual());
+ boolean shouldExpand = newWidth < maxWidth && expandRatio > 0
+ && column != selectionColumn;
+ if (shouldExpand) {
+ totalRatios += expandRatio;
+ columnsToExpand.add(column);
+ }
+ reservedPixels += newWidth;
+ columnSizes.put(visibleColumns.indexOf(column), newWidth);
+ }
+
+ /*
+ * Now that we know how many pixels we need at the very least, we
+ * can distribute the remaining pixels to all columns according to
+ * their expand ratios.
+ */
+ double pixelsToDistribute = escalator.getInnerWidth()
+ - reservedPixels;
+ if (pixelsToDistribute <= 0 || totalRatios <= 0) {
+ if (pixelsToDistribute <= 0) {
+ // Set column sizes for expanding columns
+ setColumnSizes(columnSizes);
+ }
+
+ return;
+ }
+
+ /*
+ * Check for columns that hit their max width. Adjust
+ * pixelsToDistribute and totalRatios accordingly. Recheck. Stop
+ * when no new columns hit their max width
+ */
+ boolean aColumnHasMaxedOut;
+ do {
+ aColumnHasMaxedOut = false;
+ final double widthPerRatio = pixelsToDistribute / totalRatios;
+ final Iterator<Column<?, T>> i = columnsToExpand.iterator();
+ while (i.hasNext()) {
+ final Column<?, T> column = i.next();
+ final int expandRatio = getExpandRatio(column,
+ defaultExpandRatios);
+ final int columnIndex = visibleColumns.indexOf(column);
+ final double autoWidth = columnSizes.get(columnIndex);
+ final double maxWidth = getMaxWidth(column);
+ double expandedWidth = autoWidth + widthPerRatio
+ * expandRatio;
+
+ if (maxWidth <= expandedWidth) {
+ i.remove();
+ totalRatios -= expandRatio;
+ aColumnHasMaxedOut = true;
+ pixelsToDistribute -= maxWidth - autoWidth;
+ columnSizes.put(columnIndex, maxWidth);
+ }
+ }
+ } while (aColumnHasMaxedOut);
+
+ if (totalRatios <= 0 && columnsToExpand.isEmpty()) {
+ setColumnSizes(columnSizes);
+ return;
+ }
+ assert pixelsToDistribute > 0 : "We've run out of pixels to distribute ("
+ + pixelsToDistribute
+ + "px to "
+ + totalRatios
+ + " ratios between " + columnsToExpand.size() + " columns)";
+ assert totalRatios > 0 && !columnsToExpand.isEmpty() : "Bookkeeping out of sync. Ratios: "
+ + totalRatios + " Columns: " + columnsToExpand.size();
+
+ /*
+ * If we still have anything left, distribute the remaining pixels
+ * to the remaining columns.
+ */
+ final double widthPerRatio;
+ int leftOver = 0;
+ if (BrowserInfo.get().isIE8() || BrowserInfo.get().isIE9()
+ || BrowserInfo.getBrowserString().contains("PhantomJS")) {
+ // These browsers report subpixels as integers. this usually
+ // results into issues..
+ widthPerRatio = (int) (pixelsToDistribute / totalRatios);
+ leftOver = (int) (pixelsToDistribute - widthPerRatio
+ * totalRatios);
+ } else {
+ widthPerRatio = pixelsToDistribute / totalRatios;
+ }
+ for (Column<?, T> column : columnsToExpand) {
+ final int expandRatio = getExpandRatio(column,
+ defaultExpandRatios);
+ final int columnIndex = visibleColumns.indexOf(column);
+ final double autoWidth = columnSizes.get(columnIndex);
+ double totalWidth = autoWidth + widthPerRatio * expandRatio;
+ if (leftOver > 0) {
+ totalWidth += 1;
+ leftOver--;
+ }
+ columnSizes.put(columnIndex, totalWidth);
+
+ totalRatios -= expandRatio;
+ }
+ assert totalRatios == 0 : "Bookkeeping error: there were still some ratios left undistributed: "
+ + totalRatios;
+
+ /*
+ * Check the guarantees for minimum width and scoot back the columns
+ * that don't care.
+ */
+ boolean minWidthsCausedReflows;
+ do {
+ minWidthsCausedReflows = false;
+
+ /*
+ * First, let's check which columns were too cramped, and expand
+ * them. Also keep track on how many pixels we grew - we need to
+ * remove those pixels from other columns
+ */
+ double pixelsToRemoveFromOtherColumns = 0;
+ for (Column<?, T> column : visibleColumns) {
+ /*
+ * We can't iterate over columnsToExpand, even though that
+ * would be convenient. This is because some column without
+ * an expand ratio might still have a min width - those
+ * wouldn't show up in that set.
+ */
+
+ double minWidth = getMinWidth(column);
+ final int columnIndex = visibleColumns.indexOf(column);
+ double currentWidth = columnSizes.get(columnIndex);
+ boolean hasAutoWidth = column.getWidth() < 0;
+ if (hasAutoWidth && currentWidth < minWidth) {
+ columnSizes.put(columnIndex, minWidth);
+ pixelsToRemoveFromOtherColumns += (minWidth - currentWidth);
+ minWidthsCausedReflows = true;
+
+ /*
+ * Remove this column form the set if it exists. This
+ * way we make sure that it doesn't get shrunk in the
+ * next step.
+ */
+ columnsToExpand.remove(column);
+ }
+ }
+
+ /*
+ * Now we need to shrink the remaining columns according to
+ * their ratios. Recalculate the sum of remaining ratios.
+ */
+ totalRatios = 0;
+ for (Column<?, ?> column : columnsToExpand) {
+ totalRatios += getExpandRatio(column, defaultExpandRatios);
+ }
+ final double pixelsToRemovePerRatio = pixelsToRemoveFromOtherColumns
+ / totalRatios;
+ for (Column<?, T> column : columnsToExpand) {
+ final double pixelsToRemove = pixelsToRemovePerRatio
+ * getExpandRatio(column, defaultExpandRatios);
+ int colIndex = visibleColumns.indexOf(column);
+ columnSizes.put(colIndex, columnSizes.get(colIndex)
+ - pixelsToRemove);
+ }
+
+ } while (minWidthsCausedReflows);
+
+ // Finally set all the column sizes.
+ setColumnSizes(columnSizes);
+ }
+
+ private void setColumnSizes(Map<Integer, Double> columnSizes) {
+ // Set all widths at once
+ escalator.getColumnConfiguration().setColumnWidths(columnSizes);
+ }
+
+ private int getExpandRatio(Column<?, ?> column,
+ boolean defaultExpandRatios) {
+ int expandRatio = column.getExpandRatio();
+ if (expandRatio > 0) {
+ return expandRatio;
+ } else if (expandRatio < 0) {
+ assert defaultExpandRatios : "No columns should've expanded";
+ return 1;
+ } else {
+ assert false : "this method should've not been called at all if expandRatio is 0";
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the maximum width of the column, or {@link Double#MAX_VALUE}
+ * if defined as negative.
+ */
+ private double getMaxWidth(Column<?, ?> column) {
+ double maxWidth = column.getMaximumWidth();
+ if (maxWidth >= 0) {
+ return maxWidth;
+ } else {
+ return Double.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Returns the minimum width of the column, or {@link Double#MIN_VALUE}
+ * if defined as negative.
+ */
+ private double getMinWidth(Column<?, ?> column) {
+ double minWidth = column.getMinimumWidth();
+ if (minWidth >= 0) {
+ return minWidth;
+ } else {
+ return Double.MIN_VALUE;
+ }
+ }
+
+ /**
+ * Check whether the auto width calculation is currently scheduled.
+ *
+ * @return <code>true</code> if auto width calculation is currently
+ * scheduled
+ */
+ public boolean isScheduled() {
+ return isScheduled;
+ }
+ }
+
+ private class GridSpacerUpdater implements SpacerUpdater {
+
+ private static final String STRIPE_CLASSNAME = "stripe";
+
+ private final Map<Element, Widget> elementToWidgetMap = new HashMap<Element, Widget>();
+
+ @Override
+ public void init(Spacer spacer) {
+ initTheming(spacer);
+
+ int rowIndex = spacer.getRow();
+
+ Widget detailsWidget = null;
+ try {
+ detailsWidget = detailsGenerator.getDetails(rowIndex);
+ } catch (Throwable e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Exception while generating details for row "
+ + rowIndex, e);
+ }
+
+ final double spacerHeight;
+ Element spacerElement = spacer.getElement();
+ if (detailsWidget == null) {
+ spacerElement.removeAllChildren();
+ spacerHeight = DETAILS_ROW_INITIAL_HEIGHT;
+ } else {
+ Element element = detailsWidget.getElement();
+ spacerElement.appendChild(element);
+ setParent(detailsWidget, Grid.this);
+ Widget previousWidget = elementToWidgetMap.put(element,
+ detailsWidget);
+
+ assert previousWidget == null : "Overwrote a pre-existing widget on row "
+ + rowIndex + " without proper removal first.";
+
+ /*
+ * Once we have the content properly inside the DOM, we should
+ * re-measure it to make sure that it's the correct height.
+ *
+ * This is rather tricky, since the row (tr) will get the
+ * height, but the spacer cell (td) has the borders, which
+ * should go on top of the previous row and next row.
+ */
+ double contentHeight;
+ if (detailsGenerator instanceof HeightAwareDetailsGenerator) {
+ HeightAwareDetailsGenerator sadg = (HeightAwareDetailsGenerator) detailsGenerator;
+ contentHeight = sadg.getDetailsHeight(rowIndex);
+ } else {
+ contentHeight = WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(element);
+ }
+ double borderTopAndBottomHeight = WidgetUtil
+ .getBorderTopAndBottomThickness(spacerElement);
+ double measuredHeight = contentHeight
+ + borderTopAndBottomHeight;
+ assert getElement().isOrHasChild(spacerElement) : "The spacer element wasn't in the DOM during measurement, but was assumed to be.";
+ spacerHeight = measuredHeight;
+ }
+
+ escalator.getBody().setSpacer(rowIndex, spacerHeight);
+ }
+
+ @Override
+ public void destroy(Spacer spacer) {
+ Element spacerElement = spacer.getElement();
+
+ assert getElement().isOrHasChild(spacerElement) : "Trying "
+ + "to destroy a spacer that is not connected to this "
+ + "Grid's DOM. (row: " + spacer.getRow() + ", element: "
+ + spacerElement + ")";
+
+ Widget detailsWidget = elementToWidgetMap.remove(spacerElement
+ .getFirstChildElement());
+
+ if (detailsWidget != null) {
+ /*
+ * The widget may be null here if the previous generator
+ * returned a null widget.
+ */
+
+ assert spacerElement.getFirstChild() != null : "The "
+ + "details row to destroy did not contain a widget - "
+ + "probably removed by something else without "
+ + "permission? (row: " + spacer.getRow()
+ + ", element: " + spacerElement + ")";
+
+ setParent(detailsWidget, null);
+ spacerElement.removeAllChildren();
+ }
+ }
+
+ private void initTheming(Spacer spacer) {
+ Element spacerRoot = spacer.getElement();
+
+ if (spacer.getRow() % 2 == 1) {
+ spacerRoot.getParentElement().addClassName(STRIPE_CLASSNAME);
+ } else {
+ spacerRoot.getParentElement().removeClassName(STRIPE_CLASSNAME);
+ }
+ }
+
+ }
+
+ /**
+ * Sidebar displaying toggles for hidable columns and custom widgets
+ * provided by the application.
+ * <p>
+ * The button for opening the sidebar is automatically visible inside the
+ * grid, if it contains any column hiding options or custom widgets. The
+ * column hiding toggles and custom widgets become visible once the sidebar
+ * has been opened.
+ *
+ * @since 7.5.0
+ */
+ private static class Sidebar extends Composite implements HasEnabled {
+
+ private final ClickHandler openCloseButtonHandler = new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (!isOpen()) {
+ open();
+ } else {
+ close();
+ }
+ }
+ };
+
+ private final FlowPanel rootContainer;
+
+ private final FlowPanel content;
+
+ private final MenuBar menuBar;
+
+ private final Button openCloseButton;
+
+ private final Grid<?> grid;
+
+ private Overlay overlay;
+
+ private Sidebar(Grid<?> grid) {
+ this.grid = grid;
+
+ rootContainer = new FlowPanel();
+ initWidget(rootContainer);
+
+ openCloseButton = new Button();
+
+ openCloseButton.addClickHandler(openCloseButtonHandler);
+
+ rootContainer.add(openCloseButton);
+
+ content = new FlowPanel() {
+ @Override
+ public boolean remove(Widget w) {
+ // Check here to catch child.removeFromParent() calls
+ boolean removed = super.remove(w);
+ if (removed) {
+ updateVisibility();
+ }
+
+ return removed;
+ }
+ };
+
+ createOverlay();
+
+ menuBar = new MenuBar(true) {
+
+ @Override
+ public MenuItem insertItem(MenuItem item, int beforeIndex)
+ throws IndexOutOfBoundsException {
+ if (getParent() == null) {
+ content.insert(this, 0);
+ updateVisibility();
+ }
+ return super.insertItem(item, beforeIndex);
+ }
+
+ @Override
+ public void removeItem(MenuItem item) {
+ super.removeItem(item);
+ if (getItems().isEmpty()) {
+ menuBar.removeFromParent();
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ // selecting a item with enter will lose the focus and
+ // selected item, which means that further keyboard
+ // selection won't work unless we do this:
+ if (event.getTypeInt() == Event.ONKEYDOWN
+ && event.getKeyCode() == KeyCodes.KEY_ENTER) {
+ final MenuItem item = getSelectedItem();
+ super.onBrowserEvent(event);
+ Scheduler.get().scheduleDeferred(
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ selectItem(item);
+ focus();
+ }
+ });
+
+ } else {
+ super.onBrowserEvent(event);
+ }
+ }
+
+ };
+ KeyDownHandler keyDownHandler = new KeyDownHandler() {
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+ close();
+ }
+ }
+ };
+ openCloseButton.addDomHandler(keyDownHandler,
+ KeyDownEvent.getType());
+ menuBar.addDomHandler(keyDownHandler, KeyDownEvent.getType());
+ }
+
+ /**
+ * Creates and initializes the overlay.
+ */
+ private void createOverlay() {
+ overlay = GWT.create(Overlay.class);
+ overlay.setOwner(grid);
+ overlay.setAutoHideEnabled(true);
+ overlay.addStyleDependentName("popup");
+ overlay.add(content);
+ overlay.addAutoHidePartner(rootContainer.getElement());
+ overlay.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ removeStyleName("open");
+ addStyleName("closed");
+ }
+ });
+ }
+
+ /**
+ * Opens the sidebar if not yet opened. Opening the sidebar has no
+ * effect if it is empty.
+ */
+ public void open() {
+ if (!isOpen() && isInDOM()) {
+ addStyleName("open");
+ removeStyleName("closed");
+ overlay.showRelativeTo(rootContainer);
+ }
+ }
+
+ /**
+ * Closes the sidebar if not yet closed.
+ */
+ public void close() {
+ overlay.hide();
+ }
+
+ /**
+ * Returns whether the sidebar is open or not.
+ *
+ * @return <code>true</code> if open, <code>false</code> if not
+ */
+ public boolean isOpen() {
+ return overlay != null && overlay.isShowing();
+ }
+
+ @Override
+ public void setStylePrimaryName(String styleName) {
+ super.setStylePrimaryName(styleName);
+ overlay.setStylePrimaryName(styleName);
+ content.setStylePrimaryName(styleName + "-content");
+ openCloseButton.setStylePrimaryName(styleName + "-button");
+ if (isOpen()) {
+ addStyleName("open");
+ removeStyleName("closed");
+ } else {
+ removeStyleName("open");
+ addStyleName("closed");
+ }
+ }
+
+ @Override
+ public void addStyleName(String style) {
+ super.addStyleName(style);
+ overlay.addStyleName(style);
+ }
+
+ @Override
+ public void removeStyleName(String style) {
+ super.removeStyleName(style);
+ overlay.removeStyleName(style);
+ }
+
+ private void setHeightToHeaderCellHeight() {
+ RowContainer header = grid.escalator.getHeader();
+ if (header.getRowCount() == 0
+ || !header.getRowElement(0).hasChildNodes()) {
+ getLogger()
+ .info("No header cell available when calculating sidebar button height");
+ openCloseButton.setHeight(header.getDefaultRowHeight() + "px");
+
+ return;
+ }
+
+ Element firstHeaderCell = header.getRowElement(0)
+ .getFirstChildElement();
+ double height = WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(firstHeaderCell)
+ - (WidgetUtil.measureVerticalBorder(getElement()) / 2);
+ openCloseButton.setHeight(height + "px");
+ }
+
+ private void updateVisibility() {
+ final boolean hasWidgets = content.getWidgetCount() > 0;
+ final boolean isVisible = isInDOM();
+ if (isVisible && !hasWidgets) {
+ Grid.setParent(this, null);
+ getElement().removeFromParent();
+ } else if (!isVisible && hasWidgets) {
+ close();
+ grid.getElement().appendChild(getElement());
+ Grid.setParent(this, grid);
+ // border calculation won't work until attached
+ setHeightToHeaderCellHeight();
+ }
+ }
+
+ private boolean isInDOM() {
+ return getParent() != null;
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ // make sure the button will get correct height if the button should
+ // be visible when the grid is rendered the first time.
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ setHeightToHeaderCellHeight();
+ }
+ });
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return openCloseButton.isEnabled();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (!enabled && isOpen()) {
+ close();
+ }
+
+ openCloseButton.setEnabled(enabled);
+ }
+ }
+
+ /**
+ * UI and functionality related to hiding columns with toggles in the
+ * sidebar.
+ */
+ private final class ColumnHider {
+
+ /** Map from columns to their hiding toggles, component might change */
+ private HashMap<Column<?, T>, MenuItem> columnToHidingToggleMap = new HashMap<Grid.Column<?, T>, MenuItem>();
+
+ /**
+ * When column is being hidden with a toggle, do not refresh toggles for
+ * no reason. Also helps for keeping the keyboard navigation working.
+ */
+ private boolean hidingColumn;
+
+ private void updateColumnHidable(final Column<?, T> column) {
+ if (column.isHidable()) {
+ MenuItem toggle = columnToHidingToggleMap.get(column);
+ if (toggle == null) {
+ toggle = createToggle(column);
+ }
+ toggle.setStyleName("hidden", column.isHidden());
+ } else if (columnToHidingToggleMap.containsKey(column)) {
+ sidebar.menuBar.removeItem((columnToHidingToggleMap
+ .remove(column)));
+ }
+ updateTogglesOrder();
+ }
+
+ private MenuItem createToggle(final Column<?, T> column) {
+ MenuItem toggle = new MenuItem(createHTML(column), true,
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ hidingColumn = true;
+ column.setHidden(!column.isHidden(), true);
+ hidingColumn = false;
+ }
+ });
+ toggle.addStyleName("column-hiding-toggle");
+ columnToHidingToggleMap.put(column, toggle);
+ return toggle;
+ }
+
+ private String createHTML(Column<?, T> column) {
+ final StringBuffer buf = new StringBuffer();
+ buf.append("<span class=\"");
+ if (column.isHidden()) {
+ buf.append("v-off");
+ } else {
+ buf.append("v-on");
+ }
+ buf.append("\"><div>");
+ String caption = column.getHidingToggleCaption();
+ if (caption == null) {
+ caption = column.headerCaption;
+ }
+ buf.append(caption);
+ buf.append("</div></span>");
+
+ return buf.toString();
+ }
+
+ private void updateTogglesOrder() {
+ if (!hidingColumn) {
+ int lastIndex = 0;
+ for (Column<?, T> column : getColumns()) {
+ if (column.isHidable()) {
+ final MenuItem menuItem = columnToHidingToggleMap
+ .get(column);
+ sidebar.menuBar.removeItem(menuItem);
+ sidebar.menuBar.insertItem(menuItem, lastIndex++);
+ }
+ }
+ }
+ }
+
+ private void updateHidingToggle(Column<?, T> column) {
+ if (column.isHidable()) {
+ MenuItem toggle = columnToHidingToggleMap.get(column);
+ toggle.setHTML(createHTML(column));
+ toggle.setStyleName("hidden", column.isHidden());
+ } // else we can just ignore
+ }
+
+ private void removeColumnHidingToggle(Column<?, T> column) {
+ sidebar.menuBar.removeItem(columnToHidingToggleMap.get(column));
+ }
+
+ }
+
+ /**
+ * Escalator used internally by grid to render the rows
+ */
+ private Escalator escalator = GWT.create(Escalator.class);
+
+ private final Header header = GWT.create(Header.class);
+
+ private final Footer footer = GWT.create(Footer.class);
+
+ private final Sidebar sidebar = new Sidebar(this);
+
+ /**
+ * List of columns in the grid. Order defines the visible order.
+ */
+ private List<Column<?, T>> columns = new ArrayList<Column<?, T>>();
+
+ /**
+ * The datasource currently in use. <em>Note:</em> it is <code>null</code>
+ * on initialization, but not after that.
+ */
+ private DataSource<T> dataSource;
+
+ /**
+ * Currently available row range in DataSource.
+ */
+ private Range currentDataAvailable = Range.withLength(0, 0);
+
+ /**
+ * The number of frozen columns, 0 freezes the selection column if
+ * displayed, -1 also prevents selection col from freezing.
+ */
+ private int frozenColumnCount = 0;
+
+ /**
+ * Current sort order. The (private) sort() method reads this list to
+ * determine the order in which to present rows.
+ */
+ private List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ private Renderer<Boolean> selectColumnRenderer = null;
+
+ private SelectionColumn selectionColumn;
+
+ private String rowStripeStyleName;
+ private String rowHasDataStyleName;
+ private String rowSelectedStyleName;
+ private String cellFocusStyleName;
+ private String rowFocusStyleName;
+
+ /**
+ * Current selection model.
+ */
+ private SelectionModel<T> selectionModel;
+
+ protected final CellFocusHandler cellFocusHandler;
+
+ private final UserSorter sorter = new UserSorter();
+
+ private final Editor<T> editor = GWT.create(Editor.class);
+
+ private boolean dataIsBeingFetched = false;
+
+ /**
+ * The cell a click event originated from
+ * <p>
+ * This is a workaround to make Chrome work like Firefox. In Chrome,
+ * normally if you start a drag on one cell and release on:
+ * <ul>
+ * <li>that same cell, the click event is that {@code <td>}.
+ * <li>a cell on that same row, the click event is the parent {@code <tr>}.
+ * <li>a cell on another row, the click event is the table section ancestor
+ * ({@code <thead>}, {@code <tbody>} or {@code <tfoot>}).
+ * </ul>
+ *
+ * @see #onBrowserEvent(Event)
+ */
+ private Cell cellOnPrevMouseDown;
+
+ /**
+ * A scheduled command to re-evaluate the widths of <em>all columns</em>
+ * that have calculated widths. Most probably called because
+ * minwidth/maxwidth/expandratio has changed.
+ */
+ private final AutoColumnWidthsRecalculator autoColumnWidthsRecalculator = new AutoColumnWidthsRecalculator();
+
+ private boolean enabled = true;
+
+ private DetailsGenerator detailsGenerator = DetailsGenerator.NULL;
+ private GridSpacerUpdater gridSpacerUpdater = new GridSpacerUpdater();
+ /** A set keeping track of the indices of all currently open details */
+ private Set<Integer> visibleDetails = new HashSet<Integer>();
+
+ private boolean columnReorderingAllowed;
+
+ private ColumnHider columnHider = new ColumnHider();
+
+ private DragAndDropHandler dndHandler = new DragAndDropHandler();
+
+ private AutoScroller autoScroller = new AutoScroller(this);
+
+ private DragAndDropHandler.DragAndDropCallback headerCellDndCallback = new DragAndDropCallback() {
+
+ private final AutoScrollerCallback autoScrollerCallback = new AutoScrollerCallback() {
+
+ @Override
+ public void onAutoScroll(int scrollDiff) {
+ autoScrollX = scrollDiff;
+ onDragUpdate(null);
+ }
+
+ @Override
+ public void onAutoScrollReachedMin() {
+ // make sure the drop marker is visible on the left
+ autoScrollX = 0;
+ updateDragDropMarker(clientX);
+ }
+
+ @Override
+ public void onAutoScrollReachedMax() {
+ // make sure the drop marker is visible on the right
+ autoScrollX = 0;
+ updateDragDropMarker(clientX);
+ }
+ };
+ /**
+ * Elements for displaying the dragged column(s) and drop marker
+ * properly
+ */
+ private Element table;
+ private Element tableHeader;
+ /** Marks the column drop location */
+ private Element dropMarker;
+ /** A copy of the dragged column(s), moves with cursor. */
+ private Element dragElement;
+ /** Tracks index of the column whose left side the drop would occur */
+ private int latestColumnDropIndex;
+ /**
+ * Map of possible drop positions for the column and the corresponding
+ * column index.
+ */
+ private final TreeMap<Double, Integer> possibleDropPositions = new TreeMap<Double, Integer>();
+ /**
+ * Makes sure that drag cancel doesn't cause anything unwanted like sort
+ */
+ private HandlerRegistration columnSortPreventRegistration;
+
+ private int clientX;
+
+ /** How much the grid is being auto scrolled while dragging. */
+ private int autoScrollX;
+
+ /** Captures the value of the focused column before reordering */
+ private int focusedColumnIndex;
+
+ /** Offset caused by the drag and drop marker width */
+ private double dropMarkerWidthOffset;
+
+ private void initHeaderDragElementDOM() {
+ if (table == null) {
+ tableHeader = DOM.createTHead();
+ dropMarker = DOM.createDiv();
+ tableHeader.appendChild(dropMarker);
+ table = DOM.createTable();
+ table.appendChild(tableHeader);
+ table.setClassName("header-drag-table");
+ }
+ // update the style names on each run in case primary name has been
+ // modified
+ tableHeader.setClassName(escalator.getHeader().getElement()
+ .getClassName());
+ dropMarker.setClassName(getStylePrimaryName() + "-drop-marker");
+ int topOffset = 0;
+ for (int i = 0; i < eventCell.getRowIndex(); i++) {
+ topOffset += escalator.getHeader().getRowElement(i)
+ .getFirstChildElement().getOffsetHeight();
+ }
+ tableHeader.getStyle().setTop(topOffset, Unit.PX);
+
+ getElement().appendChild(table);
+
+ dropMarkerWidthOffset = WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(dropMarker) / 2;
+ }
+
+ @Override
+ public void onDragUpdate(Event e) {
+ if (e != null) {
+ clientX = WidgetUtil.getTouchOrMouseClientX(e);
+ autoScrollX = 0;
+ }
+ resolveDragElementHorizontalPosition(clientX);
+ updateDragDropMarker(clientX);
+ }
+
+ private void updateDragDropMarker(final int clientX) {
+ final double scrollLeft = getScrollLeft();
+ final double cursorXCoordinate = clientX
+ - escalator.getHeader().getElement().getAbsoluteLeft();
+ final Entry<Double, Integer> cellEdgeOnRight = possibleDropPositions
+ .ceilingEntry(cursorXCoordinate);
+ final Entry<Double, Integer> cellEdgeOnLeft = possibleDropPositions
+ .floorEntry(cursorXCoordinate);
+ final double diffToRightEdge = cellEdgeOnRight == null ? Double.MAX_VALUE
+ : cellEdgeOnRight.getKey() - cursorXCoordinate;
+ final double diffToLeftEdge = cellEdgeOnLeft == null ? Double.MAX_VALUE
+ : cursorXCoordinate - cellEdgeOnLeft.getKey();
+
+ double dropMarkerLeft = 0 - scrollLeft;
+ if (diffToRightEdge > diffToLeftEdge) {
+ latestColumnDropIndex = cellEdgeOnLeft.getValue();
+ dropMarkerLeft += cellEdgeOnLeft.getKey();
+ } else {
+ latestColumnDropIndex = cellEdgeOnRight.getValue();
+ dropMarkerLeft += cellEdgeOnRight.getKey();
+ }
+
+ dropMarkerLeft += autoScrollX;
+
+ final double frozenColumnsWidth = autoScroller
+ .getFrozenColumnsWidth();
+ final double rightBoundaryForDrag = getSidebarBoundaryComparedTo(dropMarkerLeft);
+ final int visibleColumns = getVisibleColumns().size();
+
+ // First check if the drop marker should move left because of the
+ // sidebar opening button. this only the case if the grid is
+ // scrolled to the right
+ if (latestColumnDropIndex == visibleColumns
+ && rightBoundaryForDrag < dropMarkerLeft
+ && dropMarkerLeft <= escalator.getInnerWidth()) {
+ dropMarkerLeft = rightBoundaryForDrag - dropMarkerWidthOffset;
+ }
+
+ // Check if the drop marker shouldn't be shown at all
+ else if (dropMarkerLeft < frozenColumnsWidth
+ || dropMarkerLeft > Math.min(rightBoundaryForDrag,
+ escalator.getInnerWidth()) || dropMarkerLeft < 0) {
+ dropMarkerLeft = -10000000;
+ }
+ dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX);
+ }
+
+ private void resolveDragElementHorizontalPosition(final int clientX) {
+ double left = clientX - table.getAbsoluteLeft();
+
+ // Do not show the drag element beyond a spanned header cell
+ // limitation
+ final Double leftBound = possibleDropPositions.firstKey();
+ final Double rightBound = possibleDropPositions.lastKey();
+ final double scrollLeft = getScrollLeft();
+ if (left + scrollLeft < leftBound) {
+ left = leftBound - scrollLeft + autoScrollX;
+ } else if (left + scrollLeft > rightBound) {
+ left = rightBound - scrollLeft + autoScrollX;
+ }
+
+ // Do not show the drag element beyond the grid
+ final double sidebarBoundary = getSidebarBoundaryComparedTo(left);
+ final double gridBoundary = escalator.getInnerWidth();
+ final double rightBoundary = Math
+ .min(sidebarBoundary, gridBoundary);
+
+ // Do not show on left of the frozen columns (even if scrolled)
+ final int frozenColumnsWidth = (int) autoScroller
+ .getFrozenColumnsWidth();
+
+ left = Math.max(frozenColumnsWidth, Math.min(left, rightBoundary));
+
+ left -= dragElement.getClientWidth() / 2;
+ dragElement.getStyle().setLeft(left, Unit.PX);
+ }
+
+ private boolean isSidebarOnDraggedRow() {
+ return eventCell.getRowIndex() == 0 && sidebar.isInDOM()
+ && !sidebar.isOpen();
+ }
+
+ /**
+ * Returns the sidebar left coordinate, in relation to the grid. Or
+ * Double.MAX_VALUE if it doesn't cause a boundary.
+ */
+ private double getSidebarBoundaryComparedTo(double left) {
+ if (isSidebarOnDraggedRow()) {
+ double absoluteLeft = left + getElement().getAbsoluteLeft();
+ double sidebarLeft = sidebar.getElement().getAbsoluteLeft();
+ double diff = absoluteLeft - sidebarLeft;
+
+ if (diff > 0) {
+ return left - diff;
+ }
+ }
+ return Double.MAX_VALUE;
+ }
+
+ @Override
+ public boolean onDragStart(Event e) {
+ calculatePossibleDropPositions();
+
+ if (possibleDropPositions.isEmpty()) {
+ return false;
+ }
+
+ initHeaderDragElementDOM();
+ // needs to clone focus and sorting indicators too (UX)
+ dragElement = DOM.clone(eventCell.getElement(), true);
+ dragElement.getStyle().clearWidth();
+ dropMarker.getStyle().setProperty("height",
+ dragElement.getStyle().getHeight());
+ tableHeader.appendChild(dragElement);
+ // mark the column being dragged for styling
+ eventCell.getElement().addClassName("dragged");
+ // mark the floating cell, for styling & testing
+ dragElement.addClassName("dragged-column-header");
+
+ // start the auto scroll handler
+ autoScroller.setScrollArea(60);
+ autoScroller.start(e, ScrollAxis.HORIZONTAL, autoScrollerCallback);
+ return true;
+ }
+
+ @Override
+ public void onDragEnd() {
+ table.removeFromParent();
+ dragElement.removeFromParent();
+ eventCell.getElement().removeClassName("dragged");
+ }
+
+ @Override
+ public void onDrop() {
+ final int draggedColumnIndex = eventCell.getColumnIndex();
+ final int colspan = header.getRow(eventCell.getRowIndex())
+ .getCell(eventCell.getColumn()).getColspan();
+ if (latestColumnDropIndex != draggedColumnIndex
+ && latestColumnDropIndex != (draggedColumnIndex + colspan)) {
+ List<Column<?, T>> columns = getColumns();
+ List<Column<?, T>> reordered = new ArrayList<Column<?, T>>();
+ if (draggedColumnIndex < latestColumnDropIndex) {
+ reordered.addAll(columns.subList(0, draggedColumnIndex));
+ reordered.addAll(columns.subList(draggedColumnIndex
+ + colspan, latestColumnDropIndex));
+ reordered.addAll(columns.subList(draggedColumnIndex,
+ draggedColumnIndex + colspan));
+ reordered.addAll(columns.subList(latestColumnDropIndex,
+ columns.size()));
+ } else {
+ reordered.addAll(columns.subList(0, latestColumnDropIndex));
+ reordered.addAll(columns.subList(draggedColumnIndex,
+ draggedColumnIndex + colspan));
+ reordered.addAll(columns.subList(latestColumnDropIndex,
+ draggedColumnIndex));
+ reordered.addAll(columns.subList(draggedColumnIndex
+ + colspan, columns.size()));
+ }
+ reordered.remove(selectionColumn); // since setColumnOrder will
+ // add it anyway!
+
+ // capture focused cell column before reorder
+ Cell focusedCell = cellFocusHandler.getFocusedCell();
+ if (focusedCell != null) {
+ // take hidden columns into account
+ focusedColumnIndex = getColumns().indexOf(
+ getVisibleColumn(focusedCell.getColumn()));
+ }
+
+ Column<?, T>[] array = reordered.toArray(new Column[reordered
+ .size()]);
+ setColumnOrder(array);
+ transferCellFocusOnDrop();
+ } // else no reordering
+ }
+
+ private void transferCellFocusOnDrop() {
+ final Cell focusedCell = cellFocusHandler.getFocusedCell();
+ if (focusedCell != null) {
+ final int focusedColumnIndexDOM = focusedCell.getColumn();
+ final int focusedRowIndex = focusedCell.getRow();
+ final int draggedColumnIndex = eventCell.getColumnIndex();
+ // transfer focus if it was effected by the new column order
+ final RowContainer rowContainer = escalator
+ .findRowContainer(focusedCell.getElement());
+ if (focusedColumnIndex == draggedColumnIndex) {
+ // move with the dragged column
+ int adjustedDropIndex = latestColumnDropIndex > draggedColumnIndex ? latestColumnDropIndex - 1
+ : latestColumnDropIndex;
+ // remove hidden columns from indexing
+ adjustedDropIndex = getVisibleColumns().indexOf(
+ getColumn(adjustedDropIndex));
+ cellFocusHandler.setCellFocus(focusedRowIndex,
+ adjustedDropIndex, rowContainer);
+ } else if (latestColumnDropIndex <= focusedColumnIndex
+ && draggedColumnIndex > focusedColumnIndex) {
+ cellFocusHandler.setCellFocus(focusedRowIndex,
+ focusedColumnIndexDOM + 1, rowContainer);
+ } else if (latestColumnDropIndex > focusedColumnIndex
+ && draggedColumnIndex < focusedColumnIndex) {
+ cellFocusHandler.setCellFocus(focusedRowIndex,
+ focusedColumnIndexDOM - 1, rowContainer);
+ }
+ }
+ }
+
+ @Override
+ public void onDragCancel() {
+ // cancel next click so that we may prevent column sorting if
+ // mouse was released on top of the dragged cell
+ if (columnSortPreventRegistration == null) {
+ columnSortPreventRegistration = Event
+ .addNativePreviewHandler(new NativePreviewHandler() {
+
+ @Override
+ public void onPreviewNativeEvent(
+ NativePreviewEvent event) {
+ if (event.getTypeInt() == Event.ONCLICK) {
+ event.cancel();
+ event.getNativeEvent().preventDefault();
+ columnSortPreventRegistration
+ .removeHandler();
+ columnSortPreventRegistration = null;
+ }
+ }
+ });
+ }
+ autoScroller.stop();
+ }
+
+ /**
+ * Returns the amount of frozen columns. The selection column is always
+ * considered frozen, since it can't be moved.
+ */
+ private int getSelectionAndFrozenColumnCount() {
+ // no matter if selection column is frozen or not, it is considered
+ // frozen for column dnd reorder
+ if (getSelectionModel().getSelectionColumnRenderer() != null) {
+ return Math.max(0, getFrozenColumnCount()) + 1;
+ } else {
+ return Math.max(0, getFrozenColumnCount());
+ }
+ }
+
+ @SuppressWarnings("boxing")
+ private void calculatePossibleDropPositions() {
+ possibleDropPositions.clear();
+
+ final int draggedColumnIndex = eventCell.getColumnIndex();
+ final StaticRow<?> draggedCellRow = header.getRow(eventCell
+ .getRowIndex());
+ final int draggedColumnRightIndex = draggedColumnIndex
+ + draggedCellRow.getCell(eventCell.getColumn())
+ .getColspan();
+ final int frozenColumns = getSelectionAndFrozenColumnCount();
+ final Range draggedCellRange = Range.between(draggedColumnIndex,
+ draggedColumnRightIndex);
+ /*
+ * If the dragged cell intersects with a spanned cell in any other
+ * header or footer row, then the drag is limited inside that
+ * spanned cell. The same rules apply: the cell can't be dropped
+ * inside another spanned cell. The left and right bounds keep track
+ * of the edges of the most limiting spanned cell.
+ */
+ int leftBound = -1;
+ int rightBound = getColumnCount() + 1;
+
+ final HashSet<Integer> unavailableColumnDropIndices = new HashSet<Integer>();
+ final List<StaticRow<?>> rows = new ArrayList<StaticRow<?>>();
+ rows.addAll(header.getRows());
+ rows.addAll(footer.getRows());
+ for (StaticRow<?> row : rows) {
+ if (!row.hasSpannedCells()) {
+ continue;
+ }
+ final boolean isDraggedCellRow = row.equals(draggedCellRow);
+ for (int cellColumnIndex = frozenColumns; cellColumnIndex < getColumnCount(); cellColumnIndex++) {
+ StaticCell cell = row.getCell(getColumn(cellColumnIndex));
+ int colspan = cell.getColspan();
+ if (colspan <= 1) {
+ continue;
+ }
+ final int cellColumnRightIndex = cellColumnIndex + colspan;
+ final Range cellRange = Range.between(cellColumnIndex,
+ cellColumnRightIndex);
+ final boolean intersects = draggedCellRange
+ .intersects(cellRange);
+ if (intersects && !isDraggedCellRow) {
+ // if the currently iterated cell is inside or same as
+ // the dragged cell, then it doesn't restrict the drag
+ if (cellRange.isSubsetOf(draggedCellRange)) {
+ cellColumnIndex = cellColumnRightIndex - 1;
+ continue;
+ }
+ /*
+ * if the dragged cell is a spanned cell and it crosses
+ * with the currently iterated cell without sharing
+ * either start or end then not possible to drag the
+ * cell.
+ */
+ if (!draggedCellRange.isSubsetOf(cellRange)) {
+ return;
+ }
+ // the spanned cell overlaps the dragged cell (but is
+ // not the dragged cell)
+ if (cellColumnIndex <= draggedColumnIndex
+ && cellColumnIndex > leftBound) {
+ leftBound = cellColumnIndex;
+ }
+ if (cellColumnRightIndex < rightBound) {
+ rightBound = cellColumnRightIndex;
+ }
+ cellColumnIndex = cellColumnRightIndex - 1;
+ }
+
+ else { // can't drop inside a spanned cell, or this is the
+ // dragged cell
+ while (colspan > 1) {
+ cellColumnIndex++;
+ colspan--;
+ unavailableColumnDropIndices.add(cellColumnIndex);
+ }
+ }
+ }
+ }
+
+ if (leftBound == (rightBound - 1)) {
+ return;
+ }
+
+ double position = autoScroller.getFrozenColumnsWidth();
+ // iterate column indices and add possible drop positions
+ for (int i = frozenColumns; i < getColumnCount(); i++) {
+ Column<?, T> column = getColumn(i);
+ if (!unavailableColumnDropIndices.contains(i)
+ && !column.isHidden()) {
+ if (leftBound != -1) {
+ if (i >= leftBound && i <= rightBound) {
+ possibleDropPositions.put(position, i);
+ }
+ } else {
+ possibleDropPositions.put(position, i);
+ }
+ }
+ position += column.getWidthActual();
+ }
+
+ if (leftBound == -1) {
+ // add the right side of the last column as columns.size()
+ possibleDropPositions.put(position, getColumnCount());
+ }
+ }
+
+ };
+
+ /**
+ * Enumeration for easy setting of selection mode.
+ */
+ public enum SelectionMode {
+
+ /**
+ * Shortcut for {@link SelectionModelSingle}.
+ */
+ SINGLE {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return GWT.create(SelectionModelSingle.class);
+ }
+ },
+
+ /**
+ * Shortcut for {@link SelectionModelMulti}.
+ */
+ MULTI {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return GWT.create(SelectionModelMulti.class);
+ }
+ },
+
+ /**
+ * Shortcut for {@link SelectionModelNone}.
+ */
+ NONE {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return GWT.create(SelectionModelNone.class);
+ }
+ };
+
+ protected abstract <T> SelectionModel<T> createModel();
+ }
+
+ /**
+ * Base class for grid columns internally used by the Grid. The user should
+ * use {@link Column} when creating new columns.
+ *
+ * @param <C>
+ * the column type
+ *
+ * @param <T>
+ * the row type
+ */
+ public static abstract class Column<C, T> {
+
+ /**
+ * Default renderer for GridColumns. Renders everything into text
+ * through {@link Object#toString()}.
+ */
+ private final class DefaultTextRenderer implements Renderer<Object> {
+ boolean warned = false;
+ private final String DEFAULT_RENDERER_WARNING = "This column uses a dummy default TextRenderer. "
+ + "A more suitable renderer should be set using the setRenderer() method.";
+
+ @Override
+ public void render(RendererCellReference cell, Object data) {
+ if (!warned && !(data instanceof String)) {
+ getLogger().warning(
+ Column.this.toString() + ": "
+ + DEFAULT_RENDERER_WARNING);
+ warned = true;
+ }
+
+ final String text;
+ if (data == null) {
+ text = "";
+ } else {
+ text = data.toString();
+ }
+
+ cell.getElement().setInnerText(text);
+ }
+ }
+
+ /**
+ * the column is associated with
+ */
+ private Grid<T> grid;
+
+ /**
+ * Width of column in pixels as {@link #setWidth(double)} has been
+ * called
+ */
+ private double widthUser = GridConstants.DEFAULT_COLUMN_WIDTH_PX;
+
+ /**
+ * Renderer for rendering a value into the cell
+ */
+ private Renderer<? super C> bodyRenderer;
+
+ private boolean sortable = false;
+
+ private boolean editable = true;
+
+ private boolean resizable = true;
+
+ private boolean hidden = false;
+
+ private boolean hidable = false;
+
+ private String headerCaption = "";
+
+ private String hidingToggleCaption = null;
+
+ private double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH;
+ private double maximumWidthPx = GridConstants.DEFAULT_MAX_WIDTH;
+ private int expandRatio = GridConstants.DEFAULT_EXPAND_RATIO;
+
+ /**
+ * Constructs a new column with a simple TextRenderer.
+ */
+ public Column() {
+ setRenderer(new DefaultTextRenderer());
+ }
+
+ /**
+ * Constructs a new column with a simple TextRenderer.
+ *
+ * @param caption
+ * The header caption for this column
+ *
+ * @throws IllegalArgumentException
+ * if given header caption is null
+ */
+ public Column(String caption) throws IllegalArgumentException {
+ this();
+ setHeaderCaption(caption);
+ }
+
+ /**
+ * Constructs a new column with a custom renderer.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ *
+ * @throws IllegalArgumentException
+ * if given Renderer is null
+ */
+ public Column(Renderer<? super C> renderer)
+ throws IllegalArgumentException {
+ setRenderer(renderer);
+ }
+
+ /**
+ * Constructs a new column with a custom renderer.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ * @param caption
+ * The header caption for this column
+ *
+ * @throws IllegalArgumentException
+ * if given Renderer or header caption is null
+ */
+ public Column(String caption, Renderer<? super C> renderer)
+ throws IllegalArgumentException {
+ this(renderer);
+ setHeaderCaption(caption);
+ }
+
+ /**
+ * Internally used by the grid to set itself
+ *
+ * @param grid
+ */
+ private void setGrid(Grid<T> grid) {
+ if (this.grid != null && grid != null) {
+ // Trying to replace grid
+ throw new IllegalStateException("Column already is attached "
+ + "to a grid. Remove the column first from the grid "
+ + "and then add it. (in: " + toString() + ")");
+ }
+
+ if (this.grid != null) {
+ this.grid.recalculateColumnWidths();
+ }
+ this.grid = grid;
+ if (this.grid != null) {
+ this.grid.recalculateColumnWidths();
+ }
+ }
+
+ /**
+ * Sets a header caption for this column.
+ *
+ * @param caption
+ * The header caption for this column
+ * @return the column itself
+ *
+ */
+ public Column<C, T> setHeaderCaption(String caption) {
+ if (caption == null) {
+ caption = "";
+ }
+
+ if (!this.headerCaption.equals(caption)) {
+ this.headerCaption = caption;
+ if (grid != null) {
+ updateHeader();
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Returns the current header caption for this column
+ *
+ * @since 7.6
+ * @return the header caption string
+ */
+ public String getHeaderCaption() {
+ return headerCaption;
+ }
+
+ private void updateHeader() {
+ HeaderRow row = grid.getHeader().getDefaultRow();
+ if (row != null) {
+ row.getCell(this).setText(headerCaption);
+ if (isHidable()) {
+ grid.columnHider.updateHidingToggle(this);
+ }
+ }
+ }
+
+ /**
+ * Returns the data that should be rendered into the cell. By default
+ * returning Strings and Widgets are supported. If the return type is a
+ * String then it will be treated as preformatted text.
+ * <p>
+ * To support other types you will need to pass a custom renderer to the
+ * column via the column constructor.
+ *
+ * @param row
+ * The row object that provides the cell content.
+ *
+ * @return The cell content
+ */
+ public abstract C getValue(T row);
+
+ /**
+ * The renderer to render the cell with. By default renders the data as
+ * a String or adds the widget into the cell if the column type is of
+ * widget type.
+ *
+ * @return The renderer to render the cell content with
+ */
+ public Renderer<? super C> getRenderer() {
+ return bodyRenderer;
+ }
+
+ /**
+ * Sets a custom {@link Renderer} for this column.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if given Renderer is null
+ */
+ public Column<C, T> setRenderer(Renderer<? super C> renderer)
+ throws IllegalArgumentException {
+ if (renderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+
+ if (renderer != bodyRenderer) {
+ // Variables used to restore removed column.
+ boolean columnRemoved = false;
+ double widthInConfiguration = 0.0d;
+ ColumnConfiguration conf = null;
+ int index = 0;
+
+ if (grid != null
+ && (bodyRenderer instanceof WidgetRenderer || renderer instanceof WidgetRenderer)) {
+ // Column needs to be recreated.
+ index = grid.getColumns().indexOf(this);
+ conf = grid.escalator.getColumnConfiguration();
+ widthInConfiguration = conf.getColumnWidth(index);
+
+ conf.removeColumns(index, 1);
+ columnRemoved = true;
+ }
+
+ // Complex renderers need to be destroyed.
+ if (bodyRenderer instanceof ComplexRenderer) {
+ ((ComplexRenderer) bodyRenderer).destroy();
+ }
+
+ bodyRenderer = renderer;
+
+ if (columnRemoved) {
+ // Restore the column.
+ conf.insertColumns(index, 1);
+ conf.setColumnWidth(index, widthInConfiguration);
+ }
+
+ if (grid != null) {
+ grid.refreshBody();
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the pixel width of the column. Use a negative value for the grid
+ * to autosize column based on content and available space.
+ * <p>
+ * This action is done "finally", once the current execution loop
+ * returns. This is done to reduce overhead of unintentionally always
+ * recalculate all columns, when modifying several columns at once.
+ * <p>
+ * If the column is currently {@link #isHidden() hidden}, then this set
+ * width has effect only once the column has been made visible again.
+ *
+ * @param pixels
+ * the width in pixels or negative for auto sizing
+ */
+ public Column<C, T> setWidth(double pixels) {
+ if (!WidgetUtil.pixelValuesEqual(widthUser, pixels)) {
+ widthUser = pixels;
+ if (!isHidden()) {
+ scheduleColumnWidthRecalculator();
+ }
+ }
+ return this;
+ }
+
+ void doSetWidth(double pixels) {
+ assert !isHidden() : "applying width for a hidden column";
+ if (grid != null) {
+ int index = grid.getVisibleColumns().indexOf(this);
+ ColumnConfiguration conf = grid.escalator
+ .getColumnConfiguration();
+ conf.setColumnWidth(index, pixels);
+ }
+ }
+
+ /**
+ * Returns the pixel width of the column as given by the user.
+ * <p>
+ * <em>Note:</em> If a negative value was given to
+ * {@link #setWidth(double)}, that same negative value is returned here.
+ * <p>
+ * <em>Note:</em> Returns the value, even if the column is currently
+ * {@link #isHidden() hidden}.
+ *
+ * @return pixel width of the column, or a negative number if the column
+ * width has been automatically calculated.
+ * @see #setWidth(double)
+ * @see #getWidthActual()
+ */
+ public double getWidth() {
+ return widthUser;
+ }
+
+ /**
+ * Returns the effective pixel width of the column.
+ * <p>
+ * This differs from {@link #getWidth()} only when the column has been
+ * automatically resized, or when the column is currently
+ * {@link #isHidden() hidden}, when the value is 0.
+ *
+ * @return pixel width of the column.
+ */
+ public double getWidthActual() {
+ if (isHidden()) {
+ return 0;
+ }
+ return grid.escalator.getColumnConfiguration()
+ .getColumnWidthActual(
+ grid.getVisibleColumns().indexOf(this));
+ }
+
+ void reapplyWidth() {
+ scheduleColumnWidthRecalculator();
+ }
+
+ /**
+ * Sets whether the column should be sortable by the user. The grid can
+ * be sorted by a sortable column by clicking or tapping the column's
+ * default header. Programmatic sorting using the Grid#sort methods is
+ * not affected by this setting.
+ *
+ * @param sortable
+ * {@code true} if the user should be able to sort the
+ * column, {@code false} otherwise
+ * @return the column itself
+ */
+ public Column<C, T> setSortable(boolean sortable) {
+ if (this.sortable != sortable) {
+ this.sortable = sortable;
+ if (grid != null) {
+ grid.refreshHeader();
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether the user can sort the grid by this column.
+ * <p>
+ * <em>Note:</em> it is possible to sort by this column programmatically
+ * using the Grid#sort methods regardless of the returned value.
+ *
+ * @return {@code true} if the column is sortable by the user,
+ * {@code false} otherwise
+ */
+ public boolean isSortable() {
+ return sortable;
+ }
+
+ /**
+ * Sets whether this column can be resized by the user.
+ *
+ * @since 7.6
+ *
+ * @param resizable
+ * {@code true} if this column should be resizable,
+ * {@code false} otherwise
+ */
+ public Column<C, T> setResizable(boolean resizable) {
+ if (this.resizable != resizable) {
+ this.resizable = resizable;
+ if (grid != null) {
+ grid.refreshHeader();
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column can be resized by the user. Default is
+ * {@code true}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically resized using
+ * {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless
+ * of the returned value.
+ *
+ * @since 7.6
+ *
+ * @return {@code true} if this column is resizable, {@code false}
+ * otherwise
+ */
+ public boolean isResizable() {
+ return resizable;
+ }
+
+ /**
+ * Hides or shows the column. By default columns are visible before
+ * explicitly hiding them.
+ *
+ * @since 7.5.0
+ * @param hidden
+ * <code>true</code> to hide the column, <code>false</code>
+ * to show
+ */
+ public Column<C, T> setHidden(boolean hidden) {
+ setHidden(hidden, false);
+ return this;
+ }
+
+ private void setHidden(boolean hidden, boolean userOriginated) {
+ if (this.hidden != hidden) {
+ if (hidden) {
+ grid.escalator.getColumnConfiguration().removeColumns(
+ grid.getVisibleColumns().indexOf(this), 1);
+ this.hidden = hidden;
+ } else {
+ this.hidden = hidden;
+
+ final int columnIndex = grid.getVisibleColumns().indexOf(
+ this);
+ grid.escalator.getColumnConfiguration().insertColumns(
+ columnIndex, 1);
+
+ // make sure column is set to frozen if it needs to be,
+ // escalator doesn't handle situation where the added column
+ // would be the last frozen column
+ int gridFrozenColumns = grid.getFrozenColumnCount();
+ int escalatorFrozenColumns = grid.escalator
+ .getColumnConfiguration().getFrozenColumnCount();
+ if (gridFrozenColumns > escalatorFrozenColumns
+ && escalatorFrozenColumns == columnIndex) {
+ grid.escalator.getColumnConfiguration()
+ .setFrozenColumnCount(++escalatorFrozenColumns);
+ }
+ }
+ grid.columnHider.updateHidingToggle(this);
+ grid.header.updateColSpans();
+ grid.footer.updateColSpans();
+ scheduleColumnWidthRecalculator();
+ this.grid.fireEvent(new ColumnVisibilityChangeEvent<T>(this,
+ hidden, userOriginated));
+ }
+ }
+
+ /**
+ * Returns whether this column is hidden. Default is {@code false}.
+ *
+ * @since 7.5.0
+ * @return {@code true} if the column is currently hidden, {@code false}
+ * otherwise
+ */
+ public boolean isHidden() {
+ return hidden;
+ }
+
+ /**
+ * Set whether it is possible for the user to hide this column or not.
+ * Default is {@code false}.
+ * <p>
+ * <em>Note:</em> it is still possible to hide the column
+ * programmatically using {@link #setHidden(boolean)}.
+ *
+ * @since 7.5.0
+ * @param hidable
+ * {@code true} the user can hide this column, {@code false}
+ * otherwise
+ */
+ public Column<C, T> setHidable(boolean hidable) {
+ if (this.hidable != hidable) {
+ this.hidable = hidable;
+ grid.columnHider.updateColumnHidable(this);
+ }
+ return this;
+ }
+
+ /**
+ * Is it possible for the the user to hide this column. Default is
+ * {@code false}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically hidden using
+ * {@link #setHidden(boolean)} regardless of the returned value.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the user can hide the column,
+ * <code>false</code> if not
+ */
+ public boolean isHidable() {
+ return hidable;
+ }
+
+ /**
+ * Sets the hiding toggle's caption for this column. Shown in the toggle
+ * for this column in the grid's sidebar when the column is
+ * {@link #isHidable() hidable}.
+ * <p>
+ * The default value is <code>null</code>. In this case the header
+ * caption is used, see {@link #setHeaderCaption(String)}.
+ *
+ * @since 7.5.0
+ * @param hidingToggleCaption
+ * the caption for the hiding toggle for this column
+ */
+ public Column<C, T> setHidingToggleCaption(String hidingToggleCaption) {
+ this.hidingToggleCaption = hidingToggleCaption;
+ if (isHidable()) {
+ grid.columnHider.updateHidingToggle(this);
+ }
+ return this;
+ }
+
+ /**
+ * Gets the hiding toggle caption for this column.
+ *
+ * @since 7.5.0
+ * @see #setHidingToggleCaption(String)
+ * @return the hiding toggle's caption for this column
+ */
+ public String getHidingToggleCaption() {
+ return hidingToggleCaption;
+ }
+
+ @Override
+ public String toString() {
+ String details = "";
+
+ if (headerCaption != null && !headerCaption.isEmpty()) {
+ details += "header:\"" + headerCaption + "\" ";
+ } else {
+ details += "header:empty ";
+ }
+
+ if (grid != null) {
+ int index = grid.getColumns().indexOf(this);
+ if (index != -1) {
+ details += "attached:#" + index + " ";
+ } else {
+ details += "attached:unindexed ";
+ }
+ } else {
+ details += "detached ";
+ }
+
+ details += "sortable:" + sortable + " ";
+
+ return getClass().getSimpleName() + "[" + details.trim() + "]";
+ }
+
+ /**
+ * Sets the minimum width for this column.
+ * <p>
+ * This defines the minimum guaranteed pixel width of the column
+ * <em>when it is set to expand</em>.
+ * <p>
+ * This action is done "finally", once the current execution loop
+ * returns. This is done to reduce overhead of unintentionally always
+ * recalculate all columns, when modifying several columns at once.
+ *
+ * @param pixels
+ * the minimum width
+ * @return this column
+ */
+ public Column<C, T> setMinimumWidth(double pixels) {
+ final double maxwidth = getMaximumWidth();
+ if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) {
+ throw new IllegalArgumentException("New minimum width ("
+ + pixels + ") was greater than maximum width ("
+ + maxwidth + ")");
+ }
+
+ if (minimumWidthPx != pixels) {
+ minimumWidthPx = pixels;
+ scheduleColumnWidthRecalculator();
+ }
+ return this;
+ }
+
+ /**
+ * Sets the maximum width for this column.
+ * <p>
+ * This defines the maximum allowed pixel width of the column
+ * <em>when it is set to expand</em>.
+ * <p>
+ * This action is done "finally", once the current execution loop
+ * returns. This is done to reduce overhead of unintentionally always
+ * recalculate all columns, when modifying several columns at once.
+ *
+ * @param pixels
+ * the maximum width
+ * @param immediately
+ * <code>true</code> if the widths should be executed
+ * immediately (ignoring lazy loading completely), or
+ * <code>false</code> if the command should be run after a
+ * while (duplicate non-immediately invocations are ignored).
+ * @return this column
+ */
+ public Column<C, T> setMaximumWidth(double pixels) {
+ final double minwidth = getMinimumWidth();
+ if (pixels >= 0 && pixels < minwidth && minwidth >= 0) {
+ throw new IllegalArgumentException("New maximum width ("
+ + pixels + ") was less than minimum width (" + minwidth
+ + ")");
+ }
+
+ if (maximumWidthPx != pixels) {
+ maximumWidthPx = pixels;
+ scheduleColumnWidthRecalculator();
+ }
+ return this;
+ }
+
+ /**
+ * Sets the ratio with which the column expands.
+ * <p>
+ * By default, all columns expand equally (treated as if all of them had
+ * an expand ratio of 1). Once at least one column gets a defined expand
+ * ratio, the implicit expand ratio is removed, and only the defined
+ * expand ratios are taken into account.
+ * <p>
+ * If a column has a defined width ({@link #setWidth(double)}), it
+ * overrides this method's effects.
+ * <p>
+ * <em>Example:</em> A grid with three columns, with expand ratios 0, 1
+ * and 2, respectively. The column with a <strong>ratio of 0 is exactly
+ * as wide as its contents requires</strong>. The column with a ratio of
+ * 1 is as wide as it needs, <strong>plus a third of any excess
+ * space</strong>, bceause we have 3 parts total, and this column
+ * reservs only one of those. The column with a ratio of 2, is as wide
+ * as it needs to be, <strong>plus two thirds</strong> of the excess
+ * width.
+ * <p>
+ * This action is done "finally", once the current execution loop
+ * returns. This is done to reduce overhead of unintentionally always
+ * recalculate all columns, when modifying several columns at once.
+ *
+ * @param expandRatio
+ * the expand ratio of this column. {@code 0} to not have it
+ * expand at all. A negative number to clear the expand
+ * value.
+ * @return this column
+ */
+ public Column<C, T> setExpandRatio(int ratio) {
+ if (expandRatio != ratio) {
+ expandRatio = ratio;
+ scheduleColumnWidthRecalculator();
+ }
+ return this;
+ }
+
+ /**
+ * Clears the column's expand ratio.
+ * <p>
+ * Same as calling {@link #setExpandRatio(int) setExpandRatio(-1)}
+ *
+ * @return this column
+ */
+ public Column<C, T> clearExpandRatio() {
+ return setExpandRatio(-1);
+ }
+
+ /**
+ * Gets the minimum width for this column.
+ *
+ * @return the minimum width for this column
+ * @see #setMinimumWidth(double)
+ */
+ public double getMinimumWidth() {
+ return minimumWidthPx;
+ }
+
+ /**
+ * Gets the maximum width for this column.
+ *
+ * @return the maximum width for this column
+ * @see #setMaximumWidth(double)
+ */
+ public double getMaximumWidth() {
+ return maximumWidthPx;
+ }
+
+ /**
+ * Gets the expand ratio for this column.
+ *
+ * @return the expand ratio for this column
+ * @see #setExpandRatio(int)
+ */
+ public int getExpandRatio() {
+ return expandRatio;
+ }
+
+ /**
+ * Sets whether the values in this column should be editable by the user
+ * when the row editor is active. By default columns are editable.
+ *
+ * @param editable
+ * {@code true} to set this column editable, {@code false}
+ * otherwise
+ * @return this column
+ *
+ * @throws IllegalStateException
+ * if the editor is currently active
+ *
+ * @see Grid#editRow(int)
+ * @see Grid#isEditorActive()
+ */
+ public Column<C, T> setEditable(boolean editable) {
+ if (editable != this.editable && grid.isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot change column editable status while the editor is active");
+ }
+ this.editable = editable;
+ return this;
+ }
+
+ /**
+ * Returns whether the values in this column are editable by the user
+ * when the row editor is active.
+ *
+ * @return {@code true} if this column is editable, {@code false}
+ * otherwise
+ *
+ * @see #setEditable(boolean)
+ */
+ public boolean isEditable() {
+ return editable;
+ }
+
+ private void scheduleColumnWidthRecalculator() {
+ if (grid != null) {
+ grid.recalculateColumnWidths();
+ } else {
+ /*
+ * NOOP
+ *
+ * Since setGrid() will call reapplyWidths as the colum is
+ * attached to a grid, it will call setWidth, which, in turn,
+ * will call this method again. Therefore, it's guaranteed that
+ * the recalculation is scheduled eventually, once the column is
+ * attached to a grid.
+ */
+ }
+ }
+
+ /**
+ * Resets the default header cell contents to column header captions.
+ *
+ * @since 7.5.1
+ * @param cell
+ * default header cell for this column
+ */
+ protected void setDefaultHeaderContent(HeaderCell cell) {
+ cell.setText(headerCaption);
+ }
+ }
+
+ protected class BodyUpdater implements EscalatorUpdater {
+
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ int rowIndex = row.getRow();
+ rowReference.set(rowIndex, getDataSource().getRow(rowIndex),
+ row.getElement());
+ for (FlyweightCell cell : cellsToAttach) {
+ Renderer<?> renderer = findRenderer(cell);
+ if (renderer instanceof ComplexRenderer) {
+ try {
+ Column<?, T> column = getVisibleColumn(cell.getColumn());
+ rendererCellReference.set(cell,
+ getColumns().indexOf(column), column);
+ ((ComplexRenderer<?>) renderer)
+ .init(rendererCellReference);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error initing cell in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) {
+ for (FlyweightCell cell : attachedCells) {
+ Renderer<?> renderer = findRenderer(cell);
+ if (renderer instanceof WidgetRenderer) {
+ try {
+ WidgetRenderer<?, ?> widgetRenderer = (WidgetRenderer<?, ?>) renderer;
+
+ Widget widget = widgetRenderer.createWidget();
+ assert widget != null : "WidgetRenderer.createWidget() returned null. It should return a widget.";
+ assert widget.getParent() == null : "WidgetRenderer.createWidget() returned a widget which already is attached.";
+ assert cell.getElement().getChildCount() == 0 : "Cell content should be empty when adding Widget";
+
+ // Physical attach
+ cell.getElement().appendChild(widget.getElement());
+
+ // Logical attach
+ setParent(widget, Grid.this);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error attaching child widget in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) {
+ int rowIndex = row.getRow();
+ TableRowElement rowElement = row.getElement();
+ T rowData = dataSource.getRow(rowIndex);
+
+ boolean hasData = rowData != null;
+
+ /*
+ * TODO could be more efficient to build a list of all styles that
+ * should be used and update the element only once instead of
+ * attempting to update only the ones that have changed.
+ */
+
+ // Assign stylename for rows with data
+ boolean usedToHaveData = rowElement
+ .hasClassName(rowHasDataStyleName);
+
+ if (usedToHaveData != hasData) {
+ setStyleName(rowElement, rowHasDataStyleName, hasData);
+ }
+
+ boolean isEvenIndex = (row.getRow() % 2 == 0);
+ setStyleName(rowElement, rowStripeStyleName, !isEvenIndex);
+
+ rowReference.set(rowIndex, rowData, rowElement);
+
+ if (hasData) {
+ setStyleName(rowElement, rowSelectedStyleName,
+ isSelected(rowData));
+
+ if (rowStyleGenerator != null) {
+ try {
+ String rowStylename = rowStyleGenerator
+ .getStyle(rowReference);
+ setCustomStyleName(rowElement, rowStylename);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error generating styles for row "
+ + row.getRow(), e);
+ }
+ } else {
+ // Remove in case there was a generator previously
+ setCustomStyleName(rowElement, null);
+ }
+ } else if (usedToHaveData) {
+ setStyleName(rowElement, rowSelectedStyleName, false);
+
+ setCustomStyleName(rowElement, null);
+ }
+
+ cellFocusHandler.updateFocusedRowStyle(row);
+
+ for (FlyweightCell cell : cellsToUpdate) {
+ Column<?, T> column = getVisibleColumn(cell.getColumn());
+ final int columnIndex = getColumns().indexOf(column);
+
+ assert column != null : "Column was not found from cell ("
+ + cell.getColumn() + "," + cell.getRow() + ")";
+
+ cellFocusHandler.updateFocusedCellStyle(cell,
+ escalator.getBody());
+
+ if (hasData && cellStyleGenerator != null) {
+ try {
+ cellReference
+ .set(cell.getColumn(), columnIndex, column);
+ String generatedStyle = cellStyleGenerator
+ .getStyle(cellReference);
+ setCustomStyleName(cell.getElement(), generatedStyle);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error generating style for cell in column "
+ + cell.getColumn(), e);
+ }
+ } else if (hasData || usedToHaveData) {
+ setCustomStyleName(cell.getElement(), null);
+ }
+
+ Renderer renderer = column.getRenderer();
+
+ try {
+ rendererCellReference.set(cell, columnIndex, column);
+ if (renderer instanceof ComplexRenderer) {
+ // Hide cell content if needed
+ ComplexRenderer clxRenderer = (ComplexRenderer) renderer;
+ if (hasData) {
+ if (!usedToHaveData) {
+ // Prepare cell for rendering
+ clxRenderer.setContentVisible(
+ rendererCellReference, true);
+ }
+
+ Object value = column.getValue(rowData);
+ clxRenderer.render(rendererCellReference, value);
+
+ } else {
+ // Prepare cell for no data
+ clxRenderer.setContentVisible(
+ rendererCellReference, false);
+ }
+
+ } else if (hasData) {
+ // Simple renderers just render
+ Object value = column.getValue(rowData);
+ renderer.render(rendererCellReference, value);
+
+ } else {
+ // Clear cell if there is no data
+ cell.getElement().removeAllChildren();
+ }
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error rendering cell in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ for (FlyweightCell cell : cellsToDetach) {
+ Renderer<?> renderer = findRenderer(cell);
+ if (renderer instanceof WidgetRenderer) {
+ try {
+ Widget w = WidgetUtil.findWidget(cell.getElement()
+ .getFirstChildElement(), null);
+ if (w != null) {
+
+ // Logical detach
+ setParent(w, null);
+
+ // Physical detach
+ cell.getElement().removeChild(w.getElement());
+ }
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error detaching widget in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) {
+ int rowIndex = row.getRow();
+ // Passing null row data since it might not exist in the data source
+ // any more
+ rowReference.set(rowIndex, null, row.getElement());
+ for (FlyweightCell cell : detachedCells) {
+ Renderer<?> renderer = findRenderer(cell);
+ if (renderer instanceof ComplexRenderer) {
+ try {
+ Column<?, T> column = getVisibleColumn(cell.getColumn());
+ rendererCellReference.set(cell,
+ getColumns().indexOf(column), column);
+ ((ComplexRenderer) renderer)
+ .destroy(rendererCellReference);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error destroying cell in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+ }
+ }
+
+ protected class StaticSectionUpdater implements EscalatorUpdater {
+
+ private StaticSection<?> section;
+ private RowContainer container;
+
+ public StaticSectionUpdater(StaticSection<?> section,
+ RowContainer container) {
+ super();
+ this.section = section;
+ this.container = container;
+ }
+
+ @Override
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) {
+ StaticSection.StaticRow<?> staticRow = section.getRow(row.getRow());
+ final List<Column<?, T>> columns = getVisibleColumns();
+
+ setCustomStyleName(row.getElement(), staticRow.getStyleName());
+
+ for (FlyweightCell cell : cellsToUpdate) {
+ final StaticSection.StaticCell metadata = staticRow
+ .getCell(columns.get(cell.getColumn()));
+
+ // Decorate default row with sorting indicators
+ if (staticRow instanceof HeaderRow) {
+ addSortingIndicatorsToHeaderRow((HeaderRow) staticRow, cell);
+ }
+
+ // Assign colspan to cell before rendering
+ cell.setColSpan(metadata.getColspan());
+
+ Element td = cell.getElement();
+ td.removeAllChildren();
+ setCustomStyleName(td, metadata.getStyleName());
+
+ Element content;
+ // Wrap text or html content in default header to isolate
+ // the content from the possible column resize drag handle
+ // next to it
+ if (metadata.getType() != GridStaticCellType.WIDGET) {
+ content = DOM.createDiv();
+
+ if (staticRow instanceof HeaderRow) {
+ content.setClassName(getStylePrimaryName()
+ + "-column-header-content");
+ if (((HeaderRow) staticRow).isDefault()) {
+ content.setClassName(content.getClassName() + " "
+ + getStylePrimaryName()
+ + "-column-default-header-content");
+ }
+ } else if (staticRow instanceof FooterRow) {
+ content.setClassName(getStylePrimaryName()
+ + "-column-footer-content");
+ } else {
+ getLogger().severe(
+ "Unhandled static row type "
+ + staticRow.getClass()
+ .getCanonicalName());
+ }
+
+ td.appendChild(content);
+ } else {
+ content = td;
+ }
+
+ switch (metadata.getType()) {
+ case TEXT:
+ content.setInnerText(metadata.getText());
+ break;
+ case HTML:
+ content.setInnerHTML(metadata.getHtml());
+ break;
+ case WIDGET:
+ preDetach(row, Arrays.asList(cell));
+ content.setInnerHTML("");
+ postAttach(row, Arrays.asList(cell));
+ break;
+ }
+
+ // XXX: Should add only once in preAttach/postAttach or when
+ // resizable status changes
+ // Only add resize handles to default header row for now
+ if (columns.get(cell.getColumn()).isResizable()
+ && staticRow instanceof HeaderRow
+ && ((HeaderRow) staticRow).isDefault()) {
+
+ final int column = cell.getColumn();
+ DragHandle dragger = new DragHandle(getStylePrimaryName()
+ + "-column-resize-handle",
+ new DragHandleCallback() {
+
+ private Column<?, T> col = getVisibleColumn(column);
+ private double initialWidth = 0;
+ private double minCellWidth;
+
+ @Override
+ public void onUpdate(double deltaX,
+ double deltaY) {
+ col.setWidth(Math.max(minCellWidth,
+ initialWidth + deltaX));
+ }
+
+ @Override
+ public void onStart() {
+ initialWidth = col.getWidthActual();
+
+ minCellWidth = escalator
+ .getMinCellWidth(getColumns()
+ .indexOf(col));
+ for (Column<?, T> c : getColumns()) {
+ if (selectionColumn == c) {
+ // Don't modify selection column.
+ continue;
+ }
+
+ if (c.getWidth() < 0) {
+ c.setWidth(c.getWidthActual());
+ fireEvent(new ColumnResizeEvent<T>(
+ c));
+ }
+ }
+
+ WidgetUtil.setTextSelectionEnabled(
+ getElement(), false);
+ }
+
+ @Override
+ public void onComplete() {
+ fireEvent(new ColumnResizeEvent<T>(col));
+
+ WidgetUtil.setTextSelectionEnabled(
+ getElement(), true);
+ }
+
+ @Override
+ public void onCancel() {
+ col.setWidth(initialWidth);
+
+ WidgetUtil.setTextSelectionEnabled(
+ getElement(), true);
+ }
+ });
+ dragger.addTo(td);
+ }
+
+ cellFocusHandler.updateFocusedCellStyle(cell, container);
+ }
+ }
+
+ private void addSortingIndicatorsToHeaderRow(HeaderRow headerRow,
+ FlyweightCell cell) {
+
+ Element cellElement = cell.getElement();
+
+ boolean sortedBefore = cellElement.hasClassName("sort-asc")
+ || cellElement.hasClassName("sort-desc");
+
+ cleanup(cell);
+ if (!headerRow.isDefault()) {
+ // Nothing more to do if not in the default row
+ return;
+ }
+
+ final Column<?, T> column = getVisibleColumn(cell.getColumn());
+ SortOrder sortingOrder = getSortOrder(column);
+ boolean sortable = column.isSortable();
+
+ if (sortable) {
+ cellElement.addClassName("sortable");
+ }
+
+ if (!sortable || sortingOrder == null) {
+ // Only apply sorting indicators to sortable header columns
+ return;
+ }
+
+ if (SortDirection.ASCENDING == sortingOrder.getDirection()) {
+ cellElement.addClassName("sort-asc");
+ } else {
+ cellElement.addClassName("sort-desc");
+ }
+
+ int sortIndex = Grid.this.getSortOrder().indexOf(sortingOrder);
+ if (sortIndex > -1 && Grid.this.getSortOrder().size() > 1) {
+ // Show sort order indicator if column is
+ // sorted and other sorted columns also exists.
+ cellElement.setAttribute("sort-order",
+ String.valueOf(sortIndex + 1));
+ }
+
+ if (!sortedBefore) {
+ verifyColumnWidth(column);
+ }
+ }
+
+ /**
+ * Sort indicator requires a bit more space from the cell than normally.
+ * This method check that the now sorted column has enough width.
+ *
+ * @param column
+ * sorted column
+ */
+ private void verifyColumnWidth(Column<?, T> column) {
+ int colIndex = getColumns().indexOf(column);
+ double minWidth = escalator.getMinCellWidth(colIndex);
+ if (column.getWidthActual() < minWidth) {
+ // Fix column size
+ escalator.getColumnConfiguration().setColumnWidth(colIndex,
+ minWidth);
+
+ fireEvent(new ColumnResizeEvent<T>(column));
+ }
+ }
+
+ /**
+ * Finds the sort order for this column
+ */
+ private SortOrder getSortOrder(Column<?, ?> column) {
+ for (SortOrder order : Grid.this.getSortOrder()) {
+ if (order.getColumn() == column) {
+ return order;
+ }
+ }
+ return null;
+ }
+
+ private void cleanup(FlyweightCell cell) {
+ Element cellElement = cell.getElement();
+ cellElement.removeAttribute("sort-order");
+ cellElement.removeClassName("sort-desc");
+ cellElement.removeClassName("sort-asc");
+ cellElement.removeClassName("sortable");
+ }
+
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ }
+
+ @Override
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) {
+ StaticSection.StaticRow<?> gridRow = section.getRow(row.getRow());
+ List<Column<?, T>> columns = getVisibleColumns();
+
+ for (FlyweightCell cell : attachedCells) {
+ StaticSection.StaticCell metadata = gridRow.getCell(columns
+ .get(cell.getColumn()));
+ /*
+ * If the cell contains widgets that are not currently attached
+ * then attach them now.
+ */
+ if (GridStaticCellType.WIDGET.equals(metadata.getType())) {
+ final Widget widget = metadata.getWidget();
+ if (widget != null && !widget.isAttached()) {
+ getGrid().attachWidget(metadata.getWidget(),
+ cell.getElement());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ if (section.getRowCount() > row.getRow()) {
+ StaticSection.StaticRow<?> gridRow = section.getRow(row
+ .getRow());
+ List<Column<?, T>> columns = getVisibleColumns();
+ for (FlyweightCell cell : cellsToDetach) {
+ StaticSection.StaticCell metadata = gridRow.getCell(columns
+ .get(cell.getColumn()));
+
+ if (GridStaticCellType.WIDGET.equals(metadata.getType())
+ && metadata.getWidget() != null
+ && metadata.getWidget().isAttached()) {
+
+ getGrid().detachWidget(metadata.getWidget());
+ }
+ }
+ }
+ }
+
+ protected Grid getGrid() {
+ return section.grid;
+ }
+
+ @Override
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) {
+ }
+ };
+
+ /**
+ * Creates a new instance.
+ */
+ public Grid() {
+ initWidget(escalator);
+ getElement().setTabIndex(0);
+ cellFocusHandler = new CellFocusHandler();
+
+ setStylePrimaryName(STYLE_NAME);
+
+ escalator.getHeader().setEscalatorUpdater(createHeaderUpdater());
+ escalator.getBody().setEscalatorUpdater(createBodyUpdater());
+ escalator.getFooter().setEscalatorUpdater(createFooterUpdater());
+
+ header.setGrid(this);
+ HeaderRow defaultRow = header.appendRow();
+ header.setDefaultRow(defaultRow);
+
+ footer.setGrid(this);
+
+ editor.setGrid(this);
+
+ setSelectionMode(SelectionMode.SINGLE);
+
+ escalator.getBody().setSpacerUpdater(gridSpacerUpdater);
+
+ escalator.addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ fireEvent(new ScrollEvent());
+ }
+ });
+
+ escalator
+ .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() {
+ @Override
+ public void onRowVisibilityChange(
+ RowVisibilityChangeEvent event) {
+ if (dataSource != null && dataSource.size() != 0) {
+ dataIsBeingFetched = true;
+ dataSource.ensureAvailability(
+ event.getFirstVisibleRow(),
+ event.getVisibleRowCount());
+ }
+ }
+ });
+
+ // Default action on SelectionEvents. Refresh the body so changed
+ // become visible.
+ addSelectionHandler(new SelectionHandler<T>() {
+
+ @Override
+ public void onSelect(SelectionEvent<T> event) {
+ refreshBody();
+ }
+ });
+
+ // Sink header events and key events
+ sinkEvents(getHeader().getConsumedEvents());
+ sinkEvents(Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.KEYUP,
+ BrowserEvents.KEYPRESS, BrowserEvents.DBLCLICK,
+ BrowserEvents.MOUSEDOWN, BrowserEvents.CLICK));
+
+ // Make ENTER and SHIFT+ENTER in the header perform sorting
+ addHeaderKeyUpHandler(new HeaderKeyUpHandler() {
+ @Override
+ public void onKeyUp(GridKeyUpEvent event) {
+ if (event.getNativeKeyCode() != KeyCodes.KEY_ENTER) {
+ return;
+ }
+ if (getHeader().getRow(event.getFocusedCell().getRowIndex())
+ .isDefault()) {
+ // Only sort for enter on the default header
+ sorter.sort(event.getFocusedCell().getColumn(),
+ event.isShiftKeyDown());
+ }
+ }
+ });
+
+ addDataAvailableHandler(new DataAvailableHandler() {
+ @Override
+ public void onDataAvailable(DataAvailableEvent event) {
+ dataIsBeingFetched = false;
+ }
+ });
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (enabled == this.enabled) {
+ return;
+ }
+
+ this.enabled = enabled;
+ getElement().setTabIndex(enabled ? 0 : -1);
+
+ // Editor save and cancel buttons need to be disabled.
+ boolean editorOpen = editor.getState() != State.INACTIVE;
+ if (editorOpen) {
+ editor.setGridEnabled(enabled);
+ }
+
+ sidebar.setEnabled(enabled);
+
+ getEscalator().setScrollLocked(Direction.VERTICAL,
+ !enabled || editorOpen);
+ getEscalator().setScrollLocked(Direction.HORIZONTAL, !enabled);
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+ escalator.setStylePrimaryName(style);
+ editor.setStylePrimaryName(style);
+ sidebar.setStylePrimaryName(style + "-sidebar");
+ sidebar.addStyleName("v-contextmenu");
+
+ String rowStyle = getStylePrimaryName() + "-row";
+ rowHasDataStyleName = rowStyle + "-has-data";
+ rowSelectedStyleName = rowStyle + "-selected";
+ rowStripeStyleName = rowStyle + "-stripe";
+
+ cellFocusStyleName = getStylePrimaryName() + "-cell-focused";
+ rowFocusStyleName = getStylePrimaryName() + "-row-focused";
+
+ if (isAttached()) {
+ refreshHeader();
+ refreshBody();
+ refreshFooter();
+ }
+ }
+
+ /**
+ * Creates the escalator updater used to update the header rows in this
+ * grid. The updater is invoked when header rows or columns are added or
+ * removed, or the content of existing header cells is changed.
+ *
+ * @return the new header updater instance
+ *
+ * @see GridHeader
+ * @see Grid#getHeader()
+ */
+ protected EscalatorUpdater createHeaderUpdater() {
+ return new StaticSectionUpdater(header, escalator.getHeader());
+ }
+
+ /**
+ * Creates the escalator updater used to update the body rows in this grid.
+ * The updater is invoked when body rows or columns are added or removed,
+ * the content of body cells is changed, or the body is scrolled to expose
+ * previously hidden content.
+ *
+ * @return the new body updater instance
+ */
+ protected EscalatorUpdater createBodyUpdater() {
+ return new BodyUpdater();
+ }
+
+ /**
+ * Creates the escalator updater used to update the footer rows in this
+ * grid. The updater is invoked when header rows or columns are added or
+ * removed, or the content of existing header cells is changed.
+ *
+ * @return the new footer updater instance
+ *
+ * @see GridFooter
+ * @see #getFooter()
+ */
+ protected EscalatorUpdater createFooterUpdater() {
+ return new StaticSectionUpdater(footer, escalator.getFooter());
+ }
+
+ /**
+ * Refreshes header or footer rows on demand
+ *
+ * @param rows
+ * The row container
+ * @param firstRowIsVisible
+ * is the first row visible
+ * @param isHeader
+ * <code>true</code> if we refreshing the header, else assumed
+ * the footer
+ */
+ private void refreshRowContainer(RowContainer rows, StaticSection<?> section) {
+
+ // Add or Remove rows on demand
+ int rowDiff = section.getVisibleRowCount() - rows.getRowCount();
+ if (rowDiff > 0) {
+ rows.insertRows(0, rowDiff);
+ } else if (rowDiff < 0) {
+ rows.removeRows(0, -rowDiff);
+ }
+
+ // Refresh all the rows
+ if (rows.getRowCount() > 0) {
+ rows.refreshRows(0, rows.getRowCount());
+ }
+ }
+
+ /**
+ * Focus a body cell by row and column index.
+ *
+ * @param rowIndex
+ * index of row to focus
+ * @param columnIndex
+ * index of cell to focus
+ */
+ void focusCell(int rowIndex, int columnIndex) {
+ final Range rowRange = Range.between(0, dataSource.size());
+ final Range columnRange = Range.between(0, getVisibleColumns().size());
+
+ assert rowRange.contains(rowIndex) : "Illegal row index. Should be in range "
+ + rowRange;
+ assert columnRange.contains(columnIndex) : "Illegal column index. Should be in range "
+ + columnRange;
+
+ if (rowRange.contains(rowIndex) && columnRange.contains(columnIndex)) {
+ cellFocusHandler.setCellFocus(rowIndex, columnIndex,
+ escalator.getBody());
+ WidgetUtil.focus(getElement());
+ }
+ }
+
+ /**
+ * Refreshes all header rows
+ */
+ void refreshHeader() {
+ refreshRowContainer(escalator.getHeader(), header);
+ }
+
+ /**
+ * Refreshes all body rows
+ */
+ private void refreshBody() {
+ escalator.getBody().refreshRows(0, escalator.getBody().getRowCount());
+ }
+
+ /**
+ * Refreshes all footer rows
+ */
+ void refreshFooter() {
+ refreshRowContainer(escalator.getFooter(), footer);
+ }
+
+ /**
+ * Adds columns as the last columns in the grid.
+ *
+ * @param columns
+ * the columns to add
+ */
+ public void addColumns(Column<?, T>... columns) {
+ int count = getColumnCount();
+ for (Column<?, T> column : columns) {
+ addColumn(column, count++);
+ }
+ }
+
+ /**
+ * Adds a column as the last column in the grid.
+ *
+ * @param column
+ * the column to add
+ * @return given column
+ */
+ public <C extends Column<?, T>> C addColumn(C column) {
+ addColumn(column, getColumnCount());
+ return column;
+ }
+
+ /**
+ * Inserts a column into a specific position in the grid.
+ *
+ * @param index
+ * the index where the column should be inserted into
+ * @param column
+ * the column to add
+ * @return given column
+ *
+ * @throws IllegalStateException
+ * if Grid's current selection model renders a selection column,
+ * and {@code index} is 0.
+ */
+ public <C extends Column<?, T>> C addColumn(C column, int index) {
+ if (column == selectionColumn) {
+ throw new IllegalArgumentException("The selection column many "
+ + "not be added manually");
+ } else if (selectionColumn != null && index == 0) {
+ throw new IllegalStateException("A column cannot be inserted "
+ + "before the selection column");
+ }
+
+ addColumnSkipSelectionColumnCheck(column, index);
+ return column;
+ }
+
+ private void addColumnSkipSelectionColumnCheck(Column<?, T> column,
+ int index) {
+ // Register column with grid
+ columns.add(index, column);
+
+ header.addColumn(column);
+ footer.addColumn(column);
+
+ // Register this grid instance with the column
+ ((Column<?, T>) column).setGrid(this);
+
+ // Grid knows about hidden columns, Escalator only knows about what is
+ // visible so column indexes do not match
+ if (!column.isHidden()) {
+ int escalatorIndex = index;
+ for (int existingColumn = 0; existingColumn < index; existingColumn++) {
+ if (getColumn(existingColumn).isHidden()) {
+ escalatorIndex--;
+ }
+ }
+ escalator.getColumnConfiguration().insertColumns(escalatorIndex, 1);
+ }
+
+ // Reapply column width
+ column.reapplyWidth();
+
+ // Sink all renderer events
+ Set<String> events = new HashSet<String>();
+ events.addAll(getConsumedEventsForRenderer(column.getRenderer()));
+
+ if (column.isHidable()) {
+ columnHider.updateColumnHidable(column);
+ }
+
+ sinkEvents(events);
+ }
+
+ private void sinkEvents(Collection<String> events) {
+ assert events != null;
+
+ int eventsToSink = 0;
+ for (String typeName : events) {
+ int typeInt = Event.getTypeInt(typeName);
+ if (typeInt < 0) {
+ // Type not recognized by typeInt
+ sinkBitlessEvent(typeName);
+ } else {
+ eventsToSink |= typeInt;
+ }
+ }
+
+ if (eventsToSink > 0) {
+ sinkEvents(eventsToSink);
+ }
+ }
+
+ private Renderer<?> findRenderer(FlyweightCell cell) {
+ Column<?, T> column = getVisibleColumn(cell.getColumn());
+ assert column != null : "Could not find column at index:"
+ + cell.getColumn();
+ return column.getRenderer();
+ }
+
+ /**
+ * Removes a column from the grid.
+ *
+ * @param column
+ * the column to remove
+ */
+ public void removeColumn(Column<?, T> column) {
+ if (column != null && column.equals(selectionColumn)) {
+ throw new IllegalArgumentException(
+ "The selection column may not be removed manually.");
+ }
+
+ removeColumnSkipSelectionColumnCheck(column);
+ }
+
+ private void removeColumnSkipSelectionColumnCheck(Column<?, T> column) {
+ int columnIndex = columns.indexOf(column);
+
+ // Remove from column configuration
+ escalator.getColumnConfiguration().removeColumns(
+ getVisibleColumns().indexOf(column), 1);
+
+ updateFrozenColumns();
+
+ header.removeColumn(column);
+ footer.removeColumn(column);
+
+ // de-register column with grid
+ ((Column<?, T>) column).setGrid(null);
+
+ columns.remove(columnIndex);
+
+ if (column.isHidable()) {
+ columnHider.removeColumnHidingToggle(column);
+ }
+ }
+
+ /**
+ * Returns the amount of columns in the grid.
+ * <p>
+ * <em>NOTE:</em> this includes the hidden columns in the count.
+ *
+ * @return The number of columns in the grid
+ */
+ public int getColumnCount() {
+ return columns.size();
+ }
+
+ /**
+ * Returns a list columns in the grid, including hidden columns.
+ * <p>
+ * For currently visible columns, use {@link #getVisibleColumns()}.
+ *
+ * @return A unmodifiable list of the columns in the grid
+ */
+ public List<Column<?, T>> getColumns() {
+ return Collections
+ .unmodifiableList(new ArrayList<Column<?, T>>(columns));
+ }
+
+ /**
+ * Returns a list of the currently visible columns in the grid.
+ * <p>
+ * No {@link Column#isHidden() hidden} columns included.
+ *
+ * @since 7.5.0
+ * @return A unmodifiable list of the currently visible columns in the grid
+ */
+ public List<Column<?, T>> getVisibleColumns() {
+ ArrayList<Column<?, T>> visible = new ArrayList<Column<?, T>>();
+ for (Column<?, T> c : columns) {
+ if (!c.isHidden()) {
+ visible.add(c);
+ }
+ }
+ return Collections.unmodifiableList(visible);
+ }
+
+ /**
+ * Returns a column by its index in the grid.
+ * <p>
+ * <em>NOTE:</em> The indexing includes hidden columns.
+ *
+ * @param index
+ * the index of the column
+ * @return The column in the given index
+ * @throws IllegalArgumentException
+ * if the column index does not exist in the grid
+ */
+ public Column<?, T> getColumn(int index) throws IllegalArgumentException {
+ if (index < 0 || index >= columns.size()) {
+ throw new IllegalStateException("Column not found.");
+ }
+ return columns.get(index);
+ }
+
+ private Column<?, T> getVisibleColumn(int index)
+ throws IllegalArgumentException {
+ List<Column<?, T>> visibleColumns = getVisibleColumns();
+ if (index < 0 || index >= visibleColumns.size()) {
+ throw new IllegalStateException("Column not found.");
+ }
+ return visibleColumns.get(index);
+ }
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ protected Header getHeader() {
+ return header;
+ }
+
+ /**
+ * Gets the header row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return header row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public HeaderRow getHeaderRow(int rowIndex) {
+ return header.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the header section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow addHeaderRowAt(int index) {
+ return header.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the header section.
+ *
+ * @return the new row
+ * @see #prependHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow appendHeaderRow() {
+ return header.appendRow();
+ }
+
+ /**
+ * Returns the current default row of the header section. The default row is
+ * a special header row providing a user interface for sorting columns.
+ * Setting a header caption for column updates cells in the default header.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultHeaderRow() {
+ return header.getDefaultRow();
+ }
+
+ /**
+ * Gets the row count for the header section.
+ *
+ * @return row count
+ */
+ public int getHeaderRowCount() {
+ return header.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the header section.
+ *
+ * @return the new row
+ * @see #appendHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow prependHeaderRow() {
+ return header.prependRow();
+ }
+
+ /**
+ * Removes the given row from the header section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeHeaderRow(int)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(HeaderRow row) {
+ header.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the header section.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(int rowIndex) {
+ header.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the default row of the header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ * <p>
+ * Note: Setting the default header row will reset all cell contents to
+ * Column defaults.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * header does not contain the row
+ */
+ public void setDefaultHeaderRow(HeaderRow row) {
+ header.setDefaultRow(row);
+ }
+
+ /**
+ * Sets the visibility of the header section.
+ *
+ * @param visible
+ * true to show header section, false to hide
+ */
+ public void setHeaderVisible(boolean visible) {
+ header.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the header section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isHeaderVisible() {
+ return header.isVisible();
+ }
+
+ /* Grid Footers */
+
+ /**
+ * Returns the footer section of this grid. The default footer is empty.
+ *
+ * @return the footer
+ */
+ protected Footer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Gets the footer row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return footer row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public FooterRow getFooterRow(int rowIndex) {
+ return footer.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the footer section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow addFooterRowAt(int index) {
+ return footer.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the footer section.
+ *
+ * @return the new row
+ * @see #prependFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow appendFooterRow() {
+ return footer.appendRow();
+ }
+
+ /**
+ * Gets the row count for the footer.
+ *
+ * @return row count
+ */
+ public int getFooterRowCount() {
+ return footer.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the footer section.
+ *
+ * @return the new row
+ * @see #appendFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow prependFooterRow() {
+ return footer.prependRow();
+ }
+
+ /**
+ * Removes the given row from the footer section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeFooterRow(int)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(FooterRow row) {
+ footer.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the footer section.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeFooterRow(FooterRow)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(int rowIndex) {
+ footer.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the visibility of the footer section.
+ *
+ * @param visible
+ * true to show footer section, false to hide
+ */
+ public void setFooterVisible(boolean visible) {
+ footer.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the footer section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isFooterVisible() {
+ return footer.isVisible();
+ }
+
+ public Editor<T> getEditor() {
+ return editor;
+ }
+
+ protected Escalator getEscalator() {
+ return escalator;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(String height) {
+ escalator.setHeight(height);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ escalator.setWidth(width);
+ }
+
+ /**
+ * Sets the data source used by this grid.
+ *
+ * @param dataSource
+ * the data source to use, not null
+ * @throws IllegalArgumentException
+ * if <code>dataSource</code> is <code>null</code>
+ */
+ public void setDataSource(final DataSource<T> dataSource)
+ throws IllegalArgumentException {
+ if (dataSource == null) {
+ throw new IllegalArgumentException("dataSource can't be null.");
+ }
+
+ selectionModel.reset();
+
+ if (this.dataSource != null) {
+ this.dataSource.setDataChangeHandler(null);
+ }
+
+ this.dataSource = dataSource;
+ dataSource.setDataChangeHandler(new DataChangeHandler() {
+ @Override
+ public void dataUpdated(int firstIndex, int numberOfItems) {
+ escalator.getBody().refreshRows(firstIndex, numberOfItems);
+ }
+
+ @Override
+ public void dataRemoved(int firstIndex, int numberOfItems) {
+ escalator.getBody().removeRows(firstIndex, numberOfItems);
+ Range removed = Range.withLength(firstIndex, numberOfItems);
+ cellFocusHandler.rowsRemovedFromBody(removed);
+ }
+
+ @Override
+ public void dataAdded(int firstIndex, int numberOfItems) {
+ escalator.getBody().insertRows(firstIndex, numberOfItems);
+ Range added = Range.withLength(firstIndex, numberOfItems);
+ cellFocusHandler.rowsAddedToBody(added);
+ }
+
+ @Override
+ public void dataAvailable(int firstIndex, int numberOfItems) {
+ currentDataAvailable = Range.withLength(firstIndex,
+ numberOfItems);
+ fireEvent(new DataAvailableEvent(currentDataAvailable));
+ }
+
+ @Override
+ public void resetDataAndSize(int newSize) {
+ RowContainer body = escalator.getBody();
+ int oldSize = body.getRowCount();
+
+ // Hide all details.
+ Set<Integer> oldDetails = new HashSet<Integer>(visibleDetails);
+ for (int i : oldDetails) {
+ setDetailsVisible(i, false);
+ }
+
+ if (newSize > oldSize) {
+ body.insertRows(oldSize, newSize - oldSize);
+ cellFocusHandler.rowsAddedToBody(Range.withLength(oldSize,
+ newSize - oldSize));
+ } else if (newSize < oldSize) {
+ body.removeRows(newSize, oldSize - newSize);
+ cellFocusHandler.rowsRemovedFromBody(Range.withLength(
+ newSize, oldSize - newSize));
+ }
+
+ if (newSize > 0) {
+ dataIsBeingFetched = true;
+ Range visibleRowRange = escalator.getVisibleRowRange();
+ dataSource.ensureAvailability(visibleRowRange.getStart(),
+ visibleRowRange.length());
+ } else {
+ // We won't expect any data more data updates, so just make
+ // the bookkeeping happy
+ dataAvailable(0, 0);
+ }
+
+ assert body.getRowCount() == newSize;
+ }
+ });
+
+ int previousRowCount = escalator.getBody().getRowCount();
+ if (previousRowCount != 0) {
+ escalator.getBody().removeRows(0, previousRowCount);
+ }
+
+ setEscalatorSizeFromDataSource();
+ }
+
+ private void setEscalatorSizeFromDataSource() {
+ assert escalator.getBody().getRowCount() == 0;
+
+ int size = dataSource.size();
+ if (size == -1 && isAttached()) {
+ // Exact size is not yet known, start with some reasonable guess
+ // just to get an initial backend request going
+ size = getEscalator().getMaxVisibleRowCount();
+ }
+ if (size > 0) {
+ escalator.getBody().insertRows(0, size);
+ }
+ }
+
+ /**
+ * Gets the {@Link DataSource} for this Grid.
+ *
+ * @return the data source used by this grid
+ */
+ public DataSource<T> getDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * Sets the number of frozen columns in this grid. Setting the count to 0
+ * means that no data columns will be frozen, but the built-in selection
+ * checkbox column will still be frozen if it's in use. Setting the count to
+ * -1 will also disable the selection column.
+ * <p>
+ * The default value is 0.
+ *
+ * @param numberOfColumns
+ * the number of columns that should be frozen
+ *
+ * @throws IllegalArgumentException
+ * if the column count is < -1 or > the number of visible
+ * columns
+ */
+ public void setFrozenColumnCount(int numberOfColumns) {
+ if (numberOfColumns < -1 || numberOfColumns > getColumnCount()) {
+ throw new IllegalArgumentException(
+ "count must be between -1 and the current number of columns ("
+ + getColumnCount() + ")");
+ }
+
+ frozenColumnCount = numberOfColumns;
+ updateFrozenColumns();
+ }
+
+ private void updateFrozenColumns() {
+ escalator.getColumnConfiguration().setFrozenColumnCount(
+ getVisibleFrozenColumnCount());
+ }
+
+ private int getVisibleFrozenColumnCount() {
+ int numberOfColumns = getFrozenColumnCount();
+
+ // for the escalator the hidden columns are not in the frozen column
+ // count, but for grid they are. thus need to convert the index
+ for (int i = 0; i < frozenColumnCount; i++) {
+ if (getColumn(i).isHidden()) {
+ numberOfColumns--;
+ }
+ }
+
+ if (numberOfColumns == -1) {
+ numberOfColumns = 0;
+ } else if (selectionColumn != null) {
+ numberOfColumns++;
+ }
+ return numberOfColumns;
+ }
+
+ /**
+ * Gets the number of frozen columns in this grid. 0 means that no data
+ * columns will be frozen, but the built-in selection checkbox column will
+ * still be frozen if it's in use. -1 means that not even the selection
+ * column is frozen.
+ * <p>
+ * <em>NOTE:</em> This includes {@link Column#isHidden() hidden columns} in
+ * the count.
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount() {
+ return frozenColumnCount;
+ }
+
+ public HandlerRegistration addRowVisibilityChangeHandler(
+ RowVisibilityChangeHandler handler) {
+ /*
+ * Reusing Escalator's RowVisibilityChangeHandler, since a scroll
+ * concept is too abstract. e.g. the event needs to be re-sent when the
+ * widget is resized.
+ */
+ return escalator.addRowVisibilityChangeHandler(handler);
+ }
+
+ /**
+ * Scrolls to a certain row, using {@link ScrollDestination#ANY}.
+ * <p>
+ * If the details for that row are visible, those will be taken into account
+ * as well.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @throws IllegalArgumentException
+ * if rowIndex is below zero, or above the maximum value
+ * supported by the data source.
+ */
+ public void scrollToRow(int rowIndex) throws IllegalArgumentException {
+ scrollToRow(rowIndex, ScrollDestination.ANY,
+ GridConstants.DEFAULT_PADDING);
+ }
+
+ /**
+ * Scrolls to a certain row, using user-specified scroll destination.
+ * <p>
+ * If the details for that row are visible, those will be taken into account
+ * as well.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @param destination
+ * desired destination placement of scrolled-to-row. See
+ * {@link ScrollDestination} for more information.
+ * @throws IllegalArgumentException
+ * if rowIndex is below zero, or above the maximum value
+ * supported by the data source.
+ */
+ public void scrollToRow(int rowIndex, ScrollDestination destination)
+ throws IllegalArgumentException {
+ scrollToRow(rowIndex, destination,
+ destination == ScrollDestination.MIDDLE ? 0
+ : GridConstants.DEFAULT_PADDING);
+ }
+
+ /**
+ * Scrolls to a certain row using only user-specified parameters.
+ * <p>
+ * If the details for that row are visible, those will be taken into account
+ * as well.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @param destination
+ * desired destination placement of scrolled-to-row. See
+ * {@link ScrollDestination} for more information.
+ * @param paddingPx
+ * number of pixels to overscroll. Behavior depends on
+ * destination.
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero, because having a padding on a
+ * centered row is undefined behavior, or if rowIndex is below
+ * zero or above the row count of the data source.
+ */
+ private void scrollToRow(int rowIndex, ScrollDestination destination,
+ int paddingPx) throws IllegalArgumentException {
+ int maxsize = escalator.getBody().getRowCount() - 1;
+
+ if (rowIndex < 0) {
+ throw new IllegalArgumentException("Row index (" + rowIndex
+ + ") is below zero!");
+ }
+
+ if (rowIndex > maxsize) {
+ throw new IllegalArgumentException("Row index (" + rowIndex
+ + ") is above maximum (" + maxsize + ")!");
+ }
+
+ escalator.scrollToRowAndSpacer(rowIndex, destination, paddingPx);
+ }
+
+ /**
+ * Scrolls to the beginning of the very first row.
+ */
+ public void scrollToStart() {
+ scrollToRow(0, ScrollDestination.START);
+ }
+
+ /**
+ * Scrolls to the end of the very last row.
+ */
+ public void scrollToEnd() {
+ scrollToRow(escalator.getBody().getRowCount() - 1,
+ ScrollDestination.END);
+ }
+
+ /**
+ * Sets the vertical scroll offset.
+ *
+ * @param px
+ * the number of pixels this grid should be scrolled down
+ */
+ public void setScrollTop(double px) {
+ escalator.setScrollTop(px);
+ }
+
+ /**
+ * Gets the vertical scroll offset
+ *
+ * @return the number of pixels this grid is scrolled down
+ */
+ public double getScrollTop() {
+ return escalator.getScrollTop();
+ }
+
+ /**
+ * Sets the horizontal scroll offset
+ *
+ * @since 7.5.0
+ * @param px
+ * the number of pixels this grid should be scrolled right
+ */
+ public void setScrollLeft(double px) {
+ escalator.setScrollLeft(px);
+ }
+
+ /**
+ * Gets the horizontal scroll offset
+ *
+ * @return the number of pixels this grid is scrolled to the right
+ */
+ public double getScrollLeft() {
+ return escalator.getScrollLeft();
+ }
+
+ /**
+ * Returns the height of the scrollable area in pixels.
+ *
+ * @since 7.5.0
+ * @return the height of the scrollable area in pixels
+ */
+ public double getScrollHeight() {
+ return escalator.getScrollHeight();
+ }
+
+ /**
+ * Returns the width of the scrollable area in pixels.
+ *
+ * @since 7.5.0
+ * @return the width of the scrollable area in pixels.
+ */
+ public double getScrollWidth() {
+ return escalator.getScrollWidth();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(Grid.class.getName());
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead.
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInifinite(double)
+ * infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ public void setHeightByRows(double rows) throws IllegalArgumentException {
+ escalator.setHeightByRows(rows);
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * By default, it is {@value Escalator#DEFAULT_HEIGHT_BY_ROWS}.
+ *
+ * @return the amount of rows that should be shown in Grid's body, while in
+ * {@link HeightMode#ROW}.
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return escalator.getHeightByRows();
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via {@link #setHeight(String)}, and behave as a traditional Widget.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ escalator.setHeightMode(heightMode);
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return escalator.getHeightMode();
+ }
+
+ private Set<String> getConsumedEventsForRenderer(Renderer<?> renderer) {
+ Set<String> events = new HashSet<String>();
+ if (renderer instanceof ComplexRenderer) {
+ Collection<String> consumedEvents = ((ComplexRenderer<?>) renderer)
+ .getConsumedEvents();
+ if (consumedEvents != null) {
+ events.addAll(consumedEvents);
+ }
+ }
+ return events;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (!isEnabled()) {
+ return;
+ }
+
+ String eventType = event.getType();
+
+ if (eventType.equals(BrowserEvents.FOCUS)
+ || eventType.equals(BrowserEvents.BLUR)) {
+ super.onBrowserEvent(event);
+ return;
+ }
+
+ EventTarget target = event.getEventTarget();
+
+ if (!Element.is(target) || isOrContainsInSpacer(Element.as(target))) {
+ return;
+ }
+
+ Element e = Element.as(target);
+ RowContainer container = escalator.findRowContainer(e);
+ Cell cell;
+
+ if (container == null) {
+ if (eventType.equals(BrowserEvents.KEYDOWN)
+ || eventType.equals(BrowserEvents.KEYUP)
+ || eventType.equals(BrowserEvents.KEYPRESS)) {
+ cell = cellFocusHandler.getFocusedCell();
+ container = cellFocusHandler.containerWithFocus;
+ } else {
+ // Click might be in an editor cell, should still map.
+ if (editor.editorOverlay != null
+ && editor.editorOverlay.isOrHasChild(e)) {
+ container = escalator.getBody();
+ int rowIndex = editor.getRow();
+ int colIndex = editor.getElementColumn(e);
+
+ if (colIndex < 0) {
+ // Click in editor, but not for any column.
+ return;
+ }
+
+ TableCellElement cellElement = container
+ .getRowElement(rowIndex).getCells()
+ .getItem(colIndex);
+
+ cell = new Cell(rowIndex, colIndex, cellElement);
+ } else {
+ if (escalator.getElement().isOrHasChild(e)) {
+ eventCell.set(new Cell(-1, -1, null), Section.BODY);
+ // Fire native events.
+ super.onBrowserEvent(event);
+ }
+ return;
+ }
+ }
+ } else {
+ cell = container.getCell(e);
+ if (eventType.equals(BrowserEvents.MOUSEDOWN)) {
+ cellOnPrevMouseDown = cell;
+ } else if (cell == null && eventType.equals(BrowserEvents.CLICK)) {
+ /*
+ * Chrome has an interesting idea on click targets (see
+ * cellOnPrevMouseDown javadoc). Firefox, on the other hand, has
+ * the mousedown target as the click target.
+ */
+ cell = cellOnPrevMouseDown;
+ }
+ }
+
+ assert cell != null : "received " + eventType
+ + "-event with a null cell target";
+ eventCell.set(cell, getSectionFromContainer(container));
+
+ // Editor can steal focus from Grid and is still handled
+ if (isEditorEnabled() && handleEditorEvent(event, container)) {
+ return;
+ }
+
+ // Fire GridKeyEvents and GridClickEvents. Pass the event to escalator.
+ super.onBrowserEvent(event);
+
+ if (!isElementInChildWidget(e)) {
+
+ if (handleHeaderCellDragStartEvent(event, container)) {
+ return;
+ }
+
+ // Sorting through header Click / KeyUp
+ if (handleHeaderDefaultRowEvent(event, container)) {
+ return;
+ }
+
+ if (handleRendererEvent(event, container)) {
+ return;
+ }
+
+ if (handleCellFocusEvent(event, container)) {
+ return;
+ }
+ }
+ }
+
+ private Section getSectionFromContainer(RowContainer container) {
+ assert container != null : "RowContainer should not be null";
+
+ if (container == escalator.getBody()) {
+ return Section.BODY;
+ } else if (container == escalator.getFooter()) {
+ return Section.FOOTER;
+ } else if (container == escalator.getHeader()) {
+ return Section.HEADER;
+ }
+ assert false : "RowContainer was not header, footer or body.";
+ return null;
+ }
+
+ private boolean isOrContainsInSpacer(Node node) {
+ Node n = node;
+ while (n != null && n != getElement()) {
+ boolean isElement = Element.is(n);
+ if (isElement) {
+ String className = Element.as(n).getClassName();
+ if (className.contains(getStylePrimaryName() + "-spacer")) {
+ return true;
+ }
+ }
+ n = n.getParentNode();
+ }
+ return false;
+ }
+
+ private boolean isElementInChildWidget(Element e) {
+ Widget w = WidgetUtil.findWidget(e, null);
+
+ if (w == this) {
+ return false;
+ }
+
+ /*
+ * If e is directly inside this grid, but the grid is wrapped in a
+ * Composite, findWidget is not going to find this, only the wrapper.
+ * Thus we need to check its parents to see if we encounter this; if we
+ * don't, the found widget is actually a parent of this, so we should
+ * return false.
+ */
+ while (w != null && w != this) {
+ w = w.getParent();
+ }
+ return w != null;
+ }
+
+ private boolean handleEditorEvent(Event event, RowContainer container) {
+ Widget w;
+ if (editor.focusedColumnIndex < 0) {
+ w = null;
+ } else {
+ w = editor.getWidget(getColumn(editor.focusedColumnIndex));
+ }
+
+ EditorDomEvent<T> editorEvent = new EditorDomEvent<T>(event,
+ getEventCell(), w);
+
+ return getEditor().getEventHandler().handleEvent(editorEvent);
+ }
+
+ private boolean handleRendererEvent(Event event, RowContainer container) {
+
+ if (container == escalator.getBody()) {
+ Column<?, T> gridColumn = eventCell.getColumn();
+ boolean enterKey = event.getType().equals(BrowserEvents.KEYDOWN)
+ && event.getKeyCode() == KeyCodes.KEY_ENTER;
+ boolean doubleClick = event.getType()
+ .equals(BrowserEvents.DBLCLICK);
+
+ if (gridColumn.getRenderer() instanceof ComplexRenderer) {
+ ComplexRenderer<?> cplxRenderer = (ComplexRenderer<?>) gridColumn
+ .getRenderer();
+ if (cplxRenderer.getConsumedEvents().contains(event.getType())) {
+ if (cplxRenderer.onBrowserEvent(eventCell, event)) {
+ return true;
+ }
+ }
+
+ // Calls onActivate if KeyDown and Enter or double click
+ if ((enterKey || doubleClick)
+ && cplxRenderer.onActivate(eventCell)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean handleCellFocusEvent(Event event, RowContainer container) {
+ Collection<String> navigation = cellFocusHandler.getNavigationEvents();
+ if (navigation.contains(event.getType())) {
+ cellFocusHandler.handleNavigationEvent(event, eventCell);
+ }
+ return false;
+ }
+
+ private boolean handleHeaderCellDragStartEvent(Event event,
+ RowContainer container) {
+ if (!isColumnReorderingAllowed()) {
+ return false;
+ }
+ if (container != escalator.getHeader()) {
+ return false;
+ }
+ if (eventCell.getColumnIndex() < escalator.getColumnConfiguration()
+ .getFrozenColumnCount()) {
+ return false;
+ }
+
+ if (event.getTypeInt() == Event.ONMOUSEDOWN
+ && event.getButton() == NativeEvent.BUTTON_LEFT
+ || event.getTypeInt() == Event.ONTOUCHSTART) {
+ dndHandler.onDragStartOnDraggableElement(event,
+ headerCellDndCallback);
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+ return false;
+ }
+
+ private Point rowEventTouchStartingPoint;
+ private CellStyleGenerator<T> cellStyleGenerator;
+ private RowStyleGenerator<T> rowStyleGenerator;
+ private RowReference<T> rowReference = new RowReference<T>(this);
+ private CellReference<T> cellReference = new CellReference<T>(rowReference);
+ private RendererCellReference rendererCellReference = new RendererCellReference(
+ (RowReference<Object>) rowReference);
+
+ private boolean handleHeaderDefaultRowEvent(Event event,
+ RowContainer container) {
+ if (container != escalator.getHeader()) {
+ return false;
+ }
+ if (!getHeader().getRow(eventCell.getRowIndex()).isDefault()) {
+ return false;
+ }
+ if (!eventCell.getColumn().isSortable()) {
+ // Only handle sorting events if the column is sortable
+ return false;
+ }
+
+ if (BrowserEvents.MOUSEDOWN.equals(event.getType())
+ && event.getShiftKey()) {
+ // Don't select text when shift clicking on a header.
+ event.preventDefault();
+ }
+
+ if (BrowserEvents.TOUCHSTART.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ event.preventDefault();
+
+ Touch touch = event.getChangedTouches().get(0);
+ rowEventTouchStartingPoint = new Point(touch.getClientX(),
+ touch.getClientY());
+
+ sorter.sortAfterDelay(GridConstants.LONG_TAP_DELAY, true);
+
+ return true;
+
+ } else if (BrowserEvents.TOUCHMOVE.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ event.preventDefault();
+
+ Touch touch = event.getChangedTouches().get(0);
+ double diffX = Math.abs(touch.getClientX()
+ - rowEventTouchStartingPoint.getX());
+ double diffY = Math.abs(touch.getClientY()
+ - rowEventTouchStartingPoint.getY());
+
+ // Cancel long tap if finger strays too far from
+ // starting point
+ if (diffX > GridConstants.LONG_TAP_THRESHOLD
+ || diffY > GridConstants.LONG_TAP_THRESHOLD) {
+ sorter.cancelDelayedSort();
+ }
+
+ return true;
+
+ } else if (BrowserEvents.TOUCHEND.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ if (sorter.isDelayedSortScheduled()) {
+ // Not a long tap yet, perform single sort
+ sorter.cancelDelayedSort();
+ sorter.sort(eventCell.getColumn(), false);
+ }
+
+ return true;
+
+ } else if (BrowserEvents.TOUCHCANCEL.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ sorter.cancelDelayedSort();
+
+ return true;
+
+ } else if (BrowserEvents.CLICK.equals(event.getType())) {
+
+ sorter.sort(eventCell.getColumn(), event.getShiftKey());
+
+ // Click events should go onward to cell focus logic
+ return false;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
+
+ /*
+ * handles details[] (translated to spacer[] for Escalator), cell[],
+ * header[] and footer[]
+ */
+
+ // "#header[0][0]/DRAGhANDLE"
+ Element escalatorElement = escalator.getSubPartElement(subPart
+ .replaceFirst("^details\\[", "spacer["));
+
+ if (escalatorElement != null) {
+
+ int detailIdx = subPart.indexOf("/");
+ if (detailIdx > 0) {
+ String detail = subPart.substring(detailIdx + 1);
+ getLogger().severe(
+ "Looking up detail from index " + detailIdx
+ + " onward: \"" + detail + "\"");
+ if (detail.equalsIgnoreCase("content")) {
+ // XXX: Fix this to look up by class name!
+ return DOM.asOld(Element.as(escalatorElement.getChild(0)));
+ }
+ if (detail.equalsIgnoreCase("draghandle")) {
+ // XXX: Fix this to look up by class name!
+ return DOM.asOld(Element.as(escalatorElement.getChild(1)));
+ }
+ }
+
+ return DOM.asOld(escalatorElement);
+ }
+
+ SubPartArguments args = SubPartArguments.create(subPart);
+ Element editor = getSubPartElementEditor(args);
+ if (editor != null) {
+ return DOM.asOld(editor);
+ }
+
+ return null;
+ }
+
+ private Element getSubPartElementEditor(SubPartArguments args) {
+
+ if (!args.getType().equalsIgnoreCase("editor")
+ || editor.getState() != State.ACTIVE) {
+ return null;
+ }
+
+ if (args.getIndicesLength() == 0) {
+ return editor.editorOverlay;
+ } else if (args.getIndicesLength() == 1) {
+ int index = args.getIndex(0);
+ if (index >= columns.size()) {
+ return null;
+ }
+
+ escalator.scrollToColumn(index, ScrollDestination.ANY, 0);
+ Widget widget = editor.getWidget(columns.get(index));
+
+ if (widget != null) {
+ return widget.getElement();
+ }
+
+ // No widget for the column.
+ return null;
+ }
+
+ return null;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public String getSubPartName(com.google.gwt.user.client.Element subElement) {
+
+ String escalatorStructureName = escalator.getSubPartName(subElement);
+ if (escalatorStructureName != null) {
+ return escalatorStructureName.replaceFirst("^spacer", "details");
+ }
+
+ String editorName = getSubPartNameEditor(subElement);
+ if (editorName != null) {
+ return editorName;
+ }
+
+ return null;
+ }
+
+ private String getSubPartNameEditor(Element subElement) {
+
+ if (editor.getState() != State.ACTIVE
+ || !editor.editorOverlay.isOrHasChild(subElement)) {
+ return null;
+ }
+
+ int i = 0;
+ for (Column<?, T> column : columns) {
+ if (editor.getWidget(column).getElement().isOrHasChild(subElement)) {
+ return "editor[" + i + "]";
+ }
+ ++i;
+ }
+
+ return "editor";
+ }
+
+ private void setSelectColumnRenderer(
+ final Renderer<Boolean> selectColumnRenderer) {
+ if (this.selectColumnRenderer == selectColumnRenderer) {
+ return;
+ }
+
+ if (this.selectColumnRenderer != null) {
+ if (this.selectColumnRenderer instanceof ComplexRenderer) {
+ // End of Life for the old selection column renderer.
+ ((ComplexRenderer<?>) this.selectColumnRenderer).destroy();
+ }
+
+ // Clear field so frozen column logic in the remove method knows
+ // what to do
+ Column<?, T> colToRemove = selectionColumn;
+ selectionColumn = null;
+ removeColumnSkipSelectionColumnCheck(colToRemove);
+ cellFocusHandler.offsetRangeBy(-1);
+ }
+
+ this.selectColumnRenderer = selectColumnRenderer;
+
+ if (selectColumnRenderer != null) {
+ cellFocusHandler.offsetRangeBy(1);
+ selectionColumn = new SelectionColumn(selectColumnRenderer);
+
+ addColumnSkipSelectionColumnCheck(selectionColumn, 0);
+ selectionColumn.initDone();
+ } else {
+ selectionColumn = null;
+ refreshBody();
+ }
+
+ updateFrozenColumns();
+ }
+
+ /**
+ * Sets the current selection model.
+ * <p>
+ * This function will call {@link SelectionModel#setGrid(Grid)}.
+ *
+ * @param selectionModel
+ * a selection model implementation.
+ * @throws IllegalArgumentException
+ * if selection model argument is null
+ */
+ public void setSelectionModel(SelectionModel<T> selectionModel) {
+
+ if (selectionModel == null) {
+ throw new IllegalArgumentException("Selection model can't be null");
+ }
+
+ if (this.selectionModel != null) {
+ // Detach selection model from Grid.
+ this.selectionModel.setGrid(null);
+ }
+
+ this.selectionModel = selectionModel;
+ selectionModel.setGrid(this);
+ setSelectColumnRenderer(this.selectionModel
+ .getSelectionColumnRenderer());
+
+ // Refresh rendered rows to update selection, if it has changed
+ refreshBody();
+ }
+
+ /**
+ * Gets a reference to the current selection model.
+ *
+ * @return the currently used SelectionModel instance.
+ */
+ public SelectionModel<T> getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Sets current selection mode.
+ * <p>
+ * This is a shorthand method for {@link Grid#setSelectionModel}.
+ *
+ * @param mode
+ * a selection mode value
+ * @see {@link SelectionMode}.
+ */
+ public void setSelectionMode(SelectionMode mode) {
+ SelectionModel<T> model = mode.createModel();
+ setSelectionModel(model);
+ }
+
+ /**
+ * Test if a row is selected.
+ *
+ * @param row
+ * a row object
+ * @return true, if the current selection model considers the provided row
+ * object selected.
+ */
+ public boolean isSelected(T row) {
+ return selectionModel.isSelected(row);
+ }
+
+ /**
+ * Select a row using the current selection model.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} and
+ * {@link SelectionModel.Multi} are supported; for anything else, an
+ * exception will be thrown.
+ *
+ * @param row
+ * a row object
+ * @return <code>true</code> iff the current selection changed
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ public boolean select(T row) {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).select(row);
+ } else if (selectionModel instanceof SelectionModel.Multi<?>) {
+ return ((SelectionModel.Multi<T>) selectionModel)
+ .select(Collections.singleton(row));
+ } else {
+ throw new IllegalStateException("Unsupported selection model");
+ }
+ }
+
+ /**
+ * Deselect a row using the current selection model.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} and
+ * {@link SelectionModel.Multi} are supported; for anything else, an
+ * exception will be thrown.
+ *
+ * @param row
+ * a row object
+ * @return <code>true</code> iff the current selection changed
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ public boolean deselect(T row) {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).deselect(row);
+ } else if (selectionModel instanceof SelectionModel.Multi<?>) {
+ return ((SelectionModel.Multi<T>) selectionModel)
+ .deselect(Collections.singleton(row));
+ } else {
+ throw new IllegalStateException("Unsupported selection model");
+ }
+ }
+
+ /**
+ * Deselect all rows using the current selection model.
+ *
+ * @param row
+ * a row object
+ * @return <code>true</code> iff the current selection changed
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ public boolean deselectAll() {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ Single<T> single = ((SelectionModel.Single<T>) selectionModel);
+ if (single.getSelectedRow() != null) {
+ return single.deselect(single.getSelectedRow());
+ } else {
+ return false;
+ }
+ } else if (selectionModel instanceof SelectionModel.Multi<?>) {
+ return ((SelectionModel.Multi<T>) selectionModel).deselectAll();
+ } else {
+ throw new IllegalStateException("Unsupported selection model");
+ }
+ }
+
+ /**
+ * Gets last selected row from the current SelectionModel.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} are
+ * valid for this method; for anything else, use the
+ * {@link Grid#getSelectedRows()} method.
+ *
+ * @return a selected row reference, or null, if no row is selected
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single}
+ */
+ public T getSelectedRow() {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).getSelectedRow();
+ } else {
+ throw new IllegalStateException(
+ "Unsupported selection model; can not get single selected row");
+ }
+ }
+
+ /**
+ * Gets currently selected rows from the current selection model.
+ *
+ * @return a non-null collection containing all currently selected rows.
+ */
+ public Collection<T> getSelectedRows() {
+ return selectionModel.getSelectedRows();
+ }
+
+ @Override
+ public HandlerRegistration addSelectionHandler(
+ final SelectionHandler<T> handler) {
+ return addHandler(handler, SelectionEvent.getType());
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ *
+ * @param s
+ * a sort instance
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sorts the Grid data in ascending order along one column.
+ *
+ * @param column
+ * a grid column reference
+ */
+ public <C> void sort(Column<C, T> column) {
+ sort(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sorts the Grid data along one column.
+ *
+ * @param column
+ * a grid column reference
+ * @param direction
+ * a sort direction value
+ */
+ public <C> void sort(Column<C, T> column, SortDirection direction) {
+ sort(Sort.by(column, direction));
+ }
+
+ /**
+ * Sets the sort order to use. Setting this causes the Grid to re-sort
+ * itself.
+ *
+ * @param order
+ * a sort order list. If set to null, the sort order is cleared.
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, false);
+ }
+
+ /**
+ * Clears the sort order and indicators without re-sorting.
+ */
+ private void clearSortOrder() {
+ sortOrder.clear();
+ refreshHeader();
+ }
+
+ private void setSortOrder(List<SortOrder> order, boolean userOriginated) {
+ if (order != sortOrder) {
+ sortOrder.clear();
+ if (order != null) {
+ sortOrder.addAll(order);
+ }
+ }
+ sort(userOriginated);
+ }
+
+ /**
+ * Get a copy of the current sort order array.
+ *
+ * @return a copy of the current sort order array
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Finds the sorting order for this column
+ */
+ private SortOrder getSortOrder(Column<?, ?> column) {
+ for (SortOrder order : getSortOrder()) {
+ if (order.getColumn() == column) {
+ return order;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Register a GWT event handler for a sorting event. This handler gets
+ * called whenever this Grid needs its data source to provide data sorted in
+ * a specific order.
+ *
+ * @param handler
+ * a sort event handler
+ * @return the registration for the event
+ */
+ public HandlerRegistration addSortHandler(SortHandler<T> handler) {
+ return addHandler(handler, SortEvent.getType());
+ }
+
+ /**
+ * Register a GWT event handler for a select all event. This handler gets
+ * called whenever Grid needs all rows selected.
+ *
+ * @param handler
+ * a select all event handler
+ */
+ public HandlerRegistration addSelectAllHandler(SelectAllHandler<T> handler) {
+ return addHandler(handler, SelectAllEvent.getType());
+ }
+
+ /**
+ * Register a GWT event handler for a data available event. This handler
+ * gets called whenever the {@link DataSource} for this Grid has new data
+ * available.
+ * <p>
+ * This handle will be fired with the current available data after
+ * registration is done.
+ *
+ * @param handler
+ * a data available event handler
+ * @return the registartion for the event
+ */
+ public HandlerRegistration addDataAvailableHandler(
+ final DataAvailableHandler handler) {
+ // Deferred call to handler with current row range
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (!dataIsBeingFetched) {
+ handler.onDataAvailable(new DataAvailableEvent(
+ currentDataAvailable));
+ }
+ }
+ });
+ return addHandler(handler, DataAvailableEvent.TYPE);
+ }
+
+ /**
+ * Register a BodyKeyDownHandler to this Grid. The event for this handler is
+ * fired when a KeyDown event occurs while cell focus is in the Body of this
+ * Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyKeyDownHandler(BodyKeyDownHandler handler) {
+ return addHandler(handler, keyDown.getAssociatedType());
+ }
+
+ /**
+ * Register a BodyKeyUpHandler to this Grid. The event for this handler is
+ * fired when a KeyUp event occurs while cell focus is in the Body of this
+ * Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyKeyUpHandler(BodyKeyUpHandler handler) {
+ return addHandler(handler, keyUp.getAssociatedType());
+ }
+
+ /**
+ * Register a BodyKeyPressHandler to this Grid. The event for this handler
+ * is fired when a KeyPress event occurs while cell focus is in the Body of
+ * this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyKeyPressHandler(
+ BodyKeyPressHandler handler) {
+ return addHandler(handler, keyPress.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderKeyDownHandler to this Grid. The event for this handler
+ * is fired when a KeyDown event occurs while cell focus is in the Header of
+ * this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderKeyDownHandler(
+ HeaderKeyDownHandler handler) {
+ return addHandler(handler, keyDown.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderKeyUpHandler to this Grid. The event for this handler is
+ * fired when a KeyUp event occurs while cell focus is in the Header of this
+ * Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderKeyUpHandler(HeaderKeyUpHandler handler) {
+ return addHandler(handler, keyUp.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderKeyPressHandler to this Grid. The event for this handler
+ * is fired when a KeyPress event occurs while cell focus is in the Header
+ * of this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderKeyPressHandler(
+ HeaderKeyPressHandler handler) {
+ return addHandler(handler, keyPress.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterKeyDownHandler to this Grid. The event for this handler
+ * is fired when a KeyDown event occurs while cell focus is in the Footer of
+ * this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterKeyDownHandler(
+ FooterKeyDownHandler handler) {
+ return addHandler(handler, keyDown.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterKeyUpHandler to this Grid. The event for this handler is
+ * fired when a KeyUp event occurs while cell focus is in the Footer of this
+ * Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterKeyUpHandler(FooterKeyUpHandler handler) {
+ return addHandler(handler, keyUp.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterKeyPressHandler to this Grid. The event for this handler
+ * is fired when a KeyPress event occurs while cell focus is in the Footer
+ * of this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterKeyPressHandler(
+ FooterKeyPressHandler handler) {
+ return addHandler(handler, keyPress.getAssociatedType());
+ }
+
+ /**
+ * Register a BodyClickHandler to this Grid. The event for this handler is
+ * fired when a Click event occurs in the Body of this Grid.
+ *
+ * @param handler
+ * the click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyClickHandler(BodyClickHandler handler) {
+ return addHandler(handler, clickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderClickHandler to this Grid. The event for this handler is
+ * fired when a Click event occurs in the Header of this Grid.
+ *
+ * @param handler
+ * the click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderClickHandler(HeaderClickHandler handler) {
+ return addHandler(handler, clickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterClickHandler to this Grid. The event for this handler is
+ * fired when a Click event occurs in the Footer of this Grid.
+ *
+ * @param handler
+ * the click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterClickHandler(FooterClickHandler handler) {
+ return addHandler(handler, clickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a BodyDoubleClickHandler to this Grid. The event for this
+ * handler is fired when a double click event occurs in the Body of this
+ * Grid.
+ *
+ * @param handler
+ * the double click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyDoubleClickHandler(
+ BodyDoubleClickHandler handler) {
+ return addHandler(handler, doubleClickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderDoubleClickHandler to this Grid. The event for this
+ * handler is fired when a double click event occurs in the Header of this
+ * Grid.
+ *
+ * @param handler
+ * the double click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderDoubleClickHandler(
+ HeaderDoubleClickHandler handler) {
+ return addHandler(handler, doubleClickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterDoubleClickHandler to this Grid. The event for this
+ * handler is fired when a double click event occurs in the Footer of this
+ * Grid.
+ *
+ * @param handler
+ * the double click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterDoubleClickHandler(
+ FooterDoubleClickHandler handler) {
+ return addHandler(handler, doubleClickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a column reorder handler to this Grid. The event for this
+ * handler is fired when the Grid's columns are reordered.
+ *
+ * @since 7.5.0
+ * @param handler
+ * the handler for the event
+ * @return the registration for the event
+ */
+ public HandlerRegistration addColumnReorderHandler(
+ ColumnReorderHandler<T> handler) {
+ return addHandler(handler, ColumnReorderEvent.getType());
+ }
+
+ /**
+ * Register a column visibility change handler to this Grid. The event for
+ * this handler is fired when the Grid's columns change visibility.
+ *
+ * @since 7.5.0
+ * @param handler
+ * the handler for the event
+ * @return the registration for the event
+ */
+ public HandlerRegistration addColumnVisibilityChangeHandler(
+ ColumnVisibilityChangeHandler<T> handler) {
+ return addHandler(handler, ColumnVisibilityChangeEvent.getType());
+ }
+
+ /**
+ * Register a column resize handler to this Grid. The event for this handler
+ * is fired when the Grid's columns are resized.
+ *
+ * @since 7.6
+ * @param handler
+ * the handler for the event
+ * @return the registration for the event
+ */
+ public HandlerRegistration addColumnResizeHandler(
+ ColumnResizeHandler<T> handler) {
+ return addHandler(handler, ColumnResizeEvent.getType());
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(boolean userOriginated) {
+ refreshHeader();
+ fireEvent(new SortEvent<T>(this,
+ Collections.unmodifiableList(sortOrder), userOriginated));
+ }
+
+ private int getLastVisibleRowIndex() {
+ int lastRowIndex = escalator.getVisibleRowRange().getEnd();
+ int footerTop = escalator.getFooter().getElement().getAbsoluteTop();
+ Element lastRow;
+
+ do {
+ lastRow = escalator.getBody().getRowElement(--lastRowIndex);
+ } while (lastRow.getAbsoluteTop() > footerTop);
+
+ return lastRowIndex;
+ }
+
+ private int getFirstVisibleRowIndex() {
+ int firstRowIndex = escalator.getVisibleRowRange().getStart();
+ int headerBottom = escalator.getHeader().getElement()
+ .getAbsoluteBottom();
+ Element firstRow = escalator.getBody().getRowElement(firstRowIndex);
+
+ while (firstRow.getAbsoluteBottom() < headerBottom) {
+ firstRow = escalator.getBody().getRowElement(++firstRowIndex);
+ }
+
+ return firstRowIndex;
+ }
+
+ /**
+ * Adds a scroll handler to this grid
+ *
+ * @param handler
+ * the scroll handler to add
+ * @return a handler registration for the registered scroll handler
+ */
+ public HandlerRegistration addScrollHandler(ScrollHandler handler) {
+ return addHandler(handler, ScrollEvent.TYPE);
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return escalator.isWorkPending() || dataIsBeingFetched
+ || autoColumnWidthsRecalculator.isScheduled()
+ || editor.isWorkPending();
+ }
+
+ /**
+ * Returns whether columns can be reordered with drag and drop.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if columns can be reordered, false otherwise
+ */
+ public boolean isColumnReorderingAllowed() {
+ return columnReorderingAllowed;
+ }
+
+ /**
+ * Sets whether column reordering with drag and drop is allowed or not.
+ *
+ * @since 7.5.0
+ * @param columnReorderingAllowed
+ * specifies whether column reordering is allowed
+ */
+ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
+ this.columnReorderingAllowed = columnReorderingAllowed;
+ }
+
+ /**
+ * Sets a new column order for the grid. All columns which are not ordered
+ * here will remain in the order they were before as the last columns of
+ * grid.
+ *
+ * @param orderedColumns
+ * array of columns in wanted order
+ */
+ public void setColumnOrder(Column<?, T>... orderedColumns) {
+ ColumnConfiguration conf = getEscalator().getColumnConfiguration();
+
+ // Trigger ComplexRenderer.destroy for old content
+ conf.removeColumns(0, conf.getColumnCount());
+
+ List<Column<?, T>> newOrder = new ArrayList<Column<?, T>>();
+ if (selectionColumn != null) {
+ newOrder.add(selectionColumn);
+ }
+
+ int i = 0;
+ for (Column<?, T> column : orderedColumns) {
+ if (columns.contains(column)) {
+ newOrder.add(column);
+ ++i;
+ } else {
+ throw new IllegalArgumentException("Given column at index " + i
+ + " does not exist in Grid");
+ }
+ }
+
+ if (columns.size() != newOrder.size()) {
+ columns.removeAll(newOrder);
+ newOrder.addAll(columns);
+ }
+ columns = newOrder;
+
+ List<Column<?, T>> visibleColumns = getVisibleColumns();
+
+ // Do ComplexRenderer.init and render new content
+ conf.insertColumns(0, visibleColumns.size());
+
+ // Number of frozen columns should be kept same #16901
+ updateFrozenColumns();
+
+ // Update column widths.
+ for (Column<?, T> column : columns) {
+ column.reapplyWidth();
+ }
+
+ // Recalculate all the colspans
+ for (HeaderRow row : header.getRows()) {
+ row.calculateColspans();
+ }
+ for (FooterRow row : footer.getRows()) {
+ row.calculateColspans();
+ }
+
+ columnHider.updateTogglesOrder();
+
+ fireEvent(new ColumnReorderEvent<T>());
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for cells
+ *
+ * @param cellStyleGenerator
+ * the cell style generator to set, or <code>null</code> to
+ * remove a previously set generator
+ */
+ public void setCellStyleGenerator(CellStyleGenerator<T> cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ refreshBody();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for cells
+ *
+ * @return the cell style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public CellStyleGenerator<T> getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for rows
+ *
+ * @param rowStyleGenerator
+ * the row style generator to set, or <code>null</code> to remove
+ * a previously set generator
+ */
+ public void setRowStyleGenerator(RowStyleGenerator<T> rowStyleGenerator) {
+ this.rowStyleGenerator = rowStyleGenerator;
+ refreshBody();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for rows
+ *
+ * @return the row style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public RowStyleGenerator<T> getRowStyleGenerator() {
+ return rowStyleGenerator;
+ }
+
+ private static void setCustomStyleName(Element element, String styleName) {
+ assert element != null;
+
+ String oldStyleName = element
+ .getPropertyString(CUSTOM_STYLE_PROPERTY_NAME);
+
+ if (!SharedUtil.equals(oldStyleName, styleName)) {
+ if (oldStyleName != null && !oldStyleName.isEmpty()) {
+ element.removeClassName(oldStyleName);
+ }
+ if (styleName != null && !styleName.isEmpty()) {
+ element.addClassName(styleName);
+ }
+ element.setPropertyString(CUSTOM_STYLE_PROPERTY_NAME, styleName);
+ }
+
+ }
+
+ /**
+ * Opens the editor over the row with the given index.
+ *
+ * @param rowIndex
+ * the index of the row to be edited
+ *
+ * @throws IllegalStateException
+ * if the editor is not enabled
+ * @throws IllegalStateException
+ * if the editor is already in edit mode
+ */
+ public void editRow(int rowIndex) {
+ editor.editRow(rowIndex);
+ }
+
+ /**
+ * Returns whether the editor is currently open on some row.
+ *
+ * @return {@code true} if the editor is active, {@code false} otherwise.
+ */
+ public boolean isEditorActive() {
+ return editor.getState() != State.INACTIVE;
+ }
+
+ /**
+ * Saves any unsaved changes in the editor to the data source.
+ *
+ * @throws IllegalStateException
+ * if the editor is not enabled
+ * @throws IllegalStateException
+ * if the editor is not in edit mode
+ */
+ public void saveEditor() {
+ editor.save();
+ }
+
+ /**
+ * Cancels the currently active edit and hides the editor. Any changes that
+ * are not {@link #saveEditor() saved} are lost.
+ *
+ * @throws IllegalStateException
+ * if the editor is not enabled
+ * @throws IllegalStateException
+ * if the editor is not in edit mode
+ */
+ public void cancelEditor() {
+ editor.cancel();
+ }
+
+ /**
+ * Returns the handler responsible for binding data and editor widgets to
+ * the editor.
+ *
+ * @return the editor handler or null if not set
+ */
+ public EditorHandler<T> getEditorHandler() {
+ return editor.getHandler();
+ }
+
+ /**
+ * Sets the handler responsible for binding data and editor widgets to the
+ * editor.
+ *
+ * @param rowHandler
+ * the new editor handler
+ *
+ * @throws IllegalStateException
+ * if the editor is currently in edit mode
+ */
+ public void setEditorHandler(EditorHandler<T> handler) {
+ editor.setHandler(handler);
+ }
+
+ /**
+ * Returns the enabled state of the editor.
+ *
+ * @return true if editing is enabled, false otherwise
+ */
+ public boolean isEditorEnabled() {
+ return editor.isEnabled();
+ }
+
+ /**
+ * Sets the enabled state of the editor.
+ *
+ * @param enabled
+ * true to enable editing, false to disable
+ *
+ * @throws IllegalStateException
+ * if in edit mode and trying to disable
+ * @throws IllegalStateException
+ * if the editor handler is not set
+ */
+ public void setEditorEnabled(boolean enabled) {
+ editor.setEnabled(enabled);
+ }
+
+ /**
+ * Returns the editor widget associated with the given column. If the editor
+ * is not active, returns null.
+ *
+ * @param column
+ * the column
+ * @return the widget if the editor is open, null otherwise
+ */
+ public Widget getEditorWidget(Column<?, T> column) {
+ return editor.getWidget(column);
+ }
+
+ /**
+ * Sets the caption on the save button in the Grid editor.
+ *
+ * @param saveCaption
+ * the caption to set
+ * @throws IllegalArgumentException
+ * if {@code saveCaption} is {@code null}
+ */
+ public void setEditorSaveCaption(String saveCaption)
+ throws IllegalArgumentException {
+ editor.setSaveCaption(saveCaption);
+ }
+
+ /**
+ * Gets the current caption on the save button in the Grid editor.
+ *
+ * @return the current caption on the save button
+ */
+ public String getEditorSaveCaption() {
+ return editor.getSaveCaption();
+ }
+
+ /**
+ * Sets the caption on the cancel button in the Grid editor.
+ *
+ * @param cancelCaption
+ * the caption to set
+ * @throws IllegalArgumentException
+ * if {@code cancelCaption} is {@code null}
+ */
+ public void setEditorCancelCaption(String cancelCaption)
+ throws IllegalArgumentException {
+ editor.setCancelCaption(cancelCaption);
+ }
+
+ /**
+ * Gets the caption on the cancel button in the Grid editor.
+ *
+ * @return the current caption on the cancel button
+ */
+ public String getEditorCancelCaption() {
+ return editor.getCancelCaption();
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+
+ if (getEscalator().getBody().getRowCount() == 0 && dataSource != null) {
+ setEscalatorSizeFromDataSource();
+ }
+
+ // Grid was just attached to DOM. Column widths should be calculated.
+ recalculateColumnWidths();
+ }
+
+ @Override
+ protected void onDetach() {
+ Set<Integer> details = new HashSet<Integer>(visibleDetails);
+ for (int row : details) {
+ setDetailsVisible(row, false);
+ }
+
+ super.onDetach();
+ }
+
+ @Override
+ public void onResize() {
+ super.onResize();
+
+ /*
+ * Delay calculation to be deferred so Escalator can do it's magic.
+ */
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (escalator.getInnerWidth() != autoColumnWidthsRecalculator.lastCalculatedInnerWidth) {
+ recalculateColumnWidths();
+ }
+
+ // Vertical resizing could make editor positioning invalid so it
+ // needs to be recalculated on resize
+ if (isEditorActive()) {
+ editor.updateVerticalScrollPosition();
+ }
++
++ // if there is a resize, we need to refresh the body to avoid an
++ // off-by-one error which occurs when the user scrolls all the
++ // way to the bottom.
++ refreshBody();
+ }
+ });
+ }
+
+ /**
+ * Grid does not support adding Widgets this way.
+ * <p>
+ * This method is implemented only because removing widgets from Grid (added
+ * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface.
+ *
+ * @param w
+ * irrelevant
+ * @throws UnsupportedOperationException
+ * always
+ */
+ @Override
+ @Deprecated
+ public void add(Widget w) {
+ throw new UnsupportedOperationException(
+ "Cannot add widgets to Grid with this method");
+ }
+
+ /**
+ * Grid does not support clearing Widgets this way.
+ * <p>
+ * This method is implemented only because removing widgets from Grid (added
+ * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface.
+ *
+ * @throws UnsupportedOperationException
+ * always
+ */
+ @Override
+ @Deprecated
+ public void clear() {
+ throw new UnsupportedOperationException(
+ "Cannot clear widgets from Grid this way");
+ }
+
+ /**
+ * Grid does not support iterating through Widgets this way.
+ * <p>
+ * This method is implemented only because removing widgets from Grid (added
+ * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface.
+ *
+ * @return never
+ * @throws UnsupportedOperationException
+ * always
+ */
+ @Override
+ @Deprecated
+ public Iterator<Widget> iterator() {
+ throw new UnsupportedOperationException(
+ "Cannot iterate through widgets in Grid this way");
+ }
+
+ /**
+ * Grid does not support removing Widgets this way.
+ * <p>
+ * This method is implemented only because removing widgets from Grid (added
+ * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface.
+ *
+ * @return always <code>false</code>
+ */
+ @Override
+ @Deprecated
+ public boolean remove(Widget w) {
+ /*
+ * This is the method that is the sole reason to have Grid implement
+ * HasWidget - when Vaadin removes a Component from the hierarchy, the
+ * corresponding Widget will call removeFromParent() on itself. GWT will
+ * check there that its parent (i.e. Grid) implements HasWidgets, and
+ * will call this remove(Widget) method.
+ *
+ * tl;dr: all this song and dance to make sure GWT's sanity checks
+ * aren't triggered, even though they effectively do nothing interesting
+ * from Grid's perspective.
+ */
+ return false;
+ }
+
+ /**
+ * Accesses the package private method Widget#setParent()
+ *
+ * @param widget
+ * The widget to access
+ * @param parent
+ * The parent to set
+ */
+ private static native final void setParent(Widget widget, Grid<?> parent)
+ /*-{
+ widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent);
+ }-*/;
+
+ private static native final void onAttach(Widget widget)
+ /*-{
+ widget.@Widget::onAttach()();
+ }-*/;
+
+ private static native final void onDetach(Widget widget)
+ /*-{
+ widget.@Widget::onDetach()();
+ }-*/;
+
+ @Override
+ protected void doAttachChildren() {
+ if (sidebar.getParent() == this) {
+ onAttach(sidebar);
+ }
+ }
+
+ @Override
+ protected void doDetachChildren() {
+ if (sidebar.getParent() == this) {
+ onDetach(sidebar);
+ }
+ }
+
+ private void attachWidget(Widget w, Element parent) {
+ assert w.getParent() == null;
+
+ parent.appendChild(w.getElement());
+ setParent(w, this);
+ }
+
+ private void detachWidget(Widget w) {
+ assert w.getParent() == this;
+
+ setParent(w, null);
+ w.getElement().removeFromParent();
+ }
+
+ /**
+ * Resets all cached pixel sizes and reads new values from the DOM. This
+ * methods should be used e.g. when styles affecting the dimensions of
+ * elements in this grid have been changed.
+ */
+ public void resetSizesFromDom() {
+ getEscalator().resetSizesFromDom();
+ }
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @since 7.5.0
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+
+ if (detailsGenerator == null) {
+ throw new IllegalArgumentException(
+ "Details generator may not be null");
+ }
+
+ for (Integer index : visibleDetails) {
+ setDetailsVisible(index, false);
+ }
+
+ this.detailsGenerator = detailsGenerator;
+
+ // this will refresh all visible spacers
+ escalator.getBody().setSpacerUpdater(gridSpacerUpdater);
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @since 7.5.0
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailsGenerator;
+ }
+
+ /**
+ * Shows or hides the details for a specific row.
+ * <p>
+ * This method does nothing if trying to set show already-visible details,
+ * or hide already-hidden details.
+ *
+ * @since 7.5.0
+ * @param rowIndex
+ * the index of the affected row
+ * @param visible
+ * <code>true</code> to show the details, or <code>false</code>
+ * to hide them
+ * @see #isDetailsVisible(int)
+ */
+ public void setDetailsVisible(int rowIndex, boolean visible) {
+ if (DetailsGenerator.NULL.equals(detailsGenerator)) {
+ return;
+ }
+
+ Integer rowIndexInteger = Integer.valueOf(rowIndex);
+
+ /*
+ * We want to prevent opening a details row twice, so any subsequent
+ * openings (or closings) of details is a NOOP.
+ *
+ * When a details row is opened, it is given an arbitrary height
+ * (because Escalator requires a height upon opening). Only when it's
+ * opened, Escalator will ask the generator to generate a widget, which
+ * we then can measure. When measured, we correct the initial height by
+ * the original height.
+ *
+ * Without this check, we would override the measured height, and revert
+ * back to the initial, arbitrary, height which would most probably be
+ * wrong.
+ *
+ * see GridSpacerUpdater.init for implementation details.
+ */
+
+ boolean isVisible = isDetailsVisible(rowIndex);
+ if (visible && !isVisible) {
+ escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT);
+ visibleDetails.add(rowIndexInteger);
+ }
+
+ else if (!visible && isVisible) {
+ escalator.getBody().setSpacer(rowIndex, -1);
+ visibleDetails.remove(rowIndexInteger);
+ }
+ }
+
+ /**
+ * Check whether the details for a row is visible or not.
+ *
+ * @since 7.5.0
+ * @param rowIndex
+ * the index of the row for which to check details
+ * @return <code>true</code> iff the details for the given row is visible
+ * @see #setDetailsVisible(int, boolean)
+ */
+ public boolean isDetailsVisible(int rowIndex) {
+ return visibleDetails.contains(Integer.valueOf(rowIndex));
+ }
+
+ /**
+ * Requests that the column widths should be recalculated.
+ * <p>
+ * The actual recalculation is not necessarily done immediately so you
+ * cannot rely on the columns being the correct width after the call
+ * returns.
+ *
+ * @since 7.4.1
+ */
+ public void recalculateColumnWidths() {
+ autoColumnWidthsRecalculator.schedule();
+ }
+
+ /**
+ * Gets the customizable menu bar that is by default used for toggling
+ * column hidability. The application developer is allowed to add their
+ * custom items to the end of the menu, but should try to avoid modifying
+ * the items in the beginning of the menu that control the column hiding if
+ * any columns are marked as hidable. A toggle for opening the menu will be
+ * displayed whenever the menu contains at least one item.
+ *
+ * @since 7.5.0
+ * @return the menu bar
+ */
+ public MenuBar getSidebarMenu() {
+ return sidebar.menuBar;
+ }
+
+ /**
+ * Tests whether the sidebar menu is currently open.
+ *
+ * @since 7.5.0
+ * @see #getSidebarMenu()
+ * @return <code>true</code> if the sidebar is open; <code>false</code> if
+ * it is closed
+ */
+ public boolean isSidebarOpen() {
+ return sidebar.isOpen();
+ }
+
+ /**
+ * Sets whether the sidebar menu is open.
+ *
+ *
+ * @since 7.5.0
+ * @see #getSidebarMenu()
+ * @see #isSidebarOpen()
+ * @param sidebarOpen
+ * <code>true</code> to open the sidebar; <code>false</code> to
+ * close it
+ */
+ public void setSidebarOpen(boolean sidebarOpen) {
+ if (sidebarOpen) {
+ sidebar.open();
+ } else {
+ sidebar.close();
+ }
+ }
+
+ @Override
+ public int getTabIndex() {
+ return FocusUtil.getTabIndex(this);
+ }
+
+ @Override
+ public void setAccessKey(char key) {
+ FocusUtil.setAccessKey(this, key);
+ }
+
+ @Override
+ public void setFocus(boolean focused) {
+ FocusUtil.setFocus(this, focused);
+ }
+
+ @Override
+ public void setTabIndex(int index) {
+ FocusUtil.setTabIndex(this, index);
+ }
+
+ @Override
+ public void focus() {
+ setFocus(true);
+ }
+
+ /**
+ * Sets the buffered editor mode.
+ *
+ * @since 7.6
+ * @param editorUnbuffered
+ * <code>true</code> to enable buffered editor,
+ * <code>false</code> to disable it
+ */
+ public void setEditorBuffered(boolean editorBuffered) {
+ editor.setBuffered(editorBuffered);
+ }
+
+ /**
+ * Gets the buffered editor mode.
+ *
+ * @since 7.6
+ * @return <code>true</code> if buffered editor is enabled,
+ * <code>false</code> otherwise
+ */
+ public boolean isEditorBuffered() {
+ return editor.isBuffered();
+ }
+
+ /**
+ * Returns the {@link EventCellReference} for the latest event fired from
+ * this Grid.
+ * <p>
+ * Note: This cell reference will be updated when firing the next event.
+ *
+ * @since 7.5
+ * @return event cell reference
+ */
+ public EventCellReference<T> getEventCell() {
+ return eventCell;
+ }
+
+ /**
+ * Returns a CellReference for the cell to which the given element belongs
+ * to.
+ *
+ * @since 7.6
+ * @param element
+ * Element to find from the cell's content.
+ * @return CellReference or <code>null</code> if cell was not found.
+ */
+ public CellReference<T> getCellReference(Element element) {
+ RowContainer container = getEscalator().findRowContainer(element);
+ if (container != null) {
+ Cell cell = container.getCell(element);
+ if (cell != null) {
+ EventCellReference<T> cellRef = new EventCellReference<T>(this);
+ cellRef.set(cell, getSectionFromContainer(container));
+ return cellRef;
+ }
+ }
+ return null;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.annotations;
+
+import java.lang.annotation.ElementType;
++import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.vaadin.server.UIProvider;
+import com.vaadin.ui.UI;
+
+/**
+ * Marks a UI that should be retained when the user refreshed the browser
+ * window. By default, a new UI instance is created when refreshing, causing any
+ * UI state not captured in the URL or the URI fragment to get discarded. By
+ * adding this annotation to a UI class, the framework will instead reuse the
+ * current UI instance when a reload is detected.
+ * <p>
+ * Whenever a request is received that reloads a preserved UI, the UI's
+ * {@link UI#refresh(com.vaadin.server.VaadinRequest) refresh} method is invoked
+ * by the framework.
+ * <p>
+ * By using
+ * {@link UIProvider#isPreservedOnRefresh(com.vaadin.server.UICreateEvent)}, the
+ * decision can also be made dynamically based on other parameters than only
+ * whether this annotation is present on the UI class.
+ *
+ * @author Vaadin Ltd
+ * @since 7.0.0
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
++@Inherited
+public @interface PreserveOnRefresh {
+ // Empty marker annotation
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.annotations;
+
+import java.lang.annotation.ElementType;
++import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.vaadin.shared.communication.PushMode;
+import com.vaadin.shared.ui.ui.Transport;
+import com.vaadin.ui.UI;
+
+/**
+ * Configures server push for a {@link UI}. Adding <code>@Push</code> to a UI
+ * class configures the UI for automatic push. If some other push mode is
+ * desired, it can be passed as a parameter, e.g.
+ * <code>@Push(PushMode.MANUAL)</code>.
+ *
+ * @see PushMode
+ *
+ * @author Vaadin Ltd.
+ * @since 7.1
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
++@Inherited
+public @interface Push {
+ /**
+ * Returns the {@link PushMode} to use for the annotated UI. The default
+ * push mode when this annotation is present is {@link PushMode#AUTOMATIC}.
+ *
+ * @return the push mode to use
+ */
+ public PushMode value() default PushMode.AUTOMATIC;
+
+ /**
+ * Returns the transport type used for the push for the annotated UI. The
+ * default transport type when this annotation is present is
+ * {@link Transport#WEBSOCKET}.
+ *
+ * @return the transport type to use
+ */
+ public Transport transport() default Transport.WEBSOCKET;
+
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.annotations;
+
+import java.lang.annotation.ElementType;
++import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.vaadin.ui.UI;
+
+/**
+ * Defines a specific theme for a {@link UI}.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
++@Inherited
+public @interface Theme {
+ /**
+ * @return simple name of the theme
+ */
+ public String value();
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.annotations;
+
+import java.lang.annotation.ElementType;
++import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.vaadin.ui.UI;
+
+/**
+ * Defines the HTML page title for a {@link UI}.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
++@Inherited
+public @interface Title {
+ /**
+ * Gets the HTML title that should be used if the UI is used on it's own.
+ *
+ * @return a page title string
+ */
+ public String value();
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
++import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.vaadin.server.Constants;
+import com.vaadin.server.DefaultDeploymentConfiguration;
+import com.vaadin.server.DeploymentConfiguration;
+import com.vaadin.server.DeploymentConfiguration.LegacyProperyToStringMode;
+import com.vaadin.server.VaadinServlet;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.ui.UI;
+
+/**
+ * Annotation for configuring subclasses of {@link VaadinServlet}. For a
+ * {@link VaadinServlet} class that has this annotation, the defined values are
+ * read during initialization and will be available using
+ * {@link DeploymentConfiguration#getApplicationOrSystemProperty(String, String)}
+ * as well as from specific methods in {@link DeploymentConfiguration}. Init
+ * params defined in <code>web.xml</code> or the <code>@WebServlet</code>
+ * annotation take precedence over values defined in this annotation.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
++@Inherited
+public @interface VaadinServletConfiguration {
+ /**
+ * Defines the init parameter name for methods in
+ * {@link VaadinServletConfiguration}.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.METHOD)
+ @Documented
+ public @interface InitParameterName {
+ /**
+ * The name of the init parameter that the annotated method controls.
+ *
+ * @return the parameter name
+ */
+ public String value();
+ }
+
+ /**
+ * Whether Vaadin is in production mode.
+ *
+ * @return true if in production mode, false otherwise.
+ *
+ * @see DeploymentConfiguration#isProductionMode()
+ */
+ @InitParameterName(Constants.SERVLET_PARAMETER_PRODUCTION_MODE)
+ public boolean productionMode();
+
+ /**
+ * Gets the default UI class to use for the servlet.
+ *
+ * @return the default UI class
+ */
+ @InitParameterName(VaadinSession.UI_PARAMETER)
+ public Class<? extends UI> ui();
+
+ /**
+ * The time resources can be cached in the browser, in seconds. The default
+ * value is 3600 seconds, i.e. one hour.
+ *
+ * @return the resource cache time
+ *
+ * @see DeploymentConfiguration#getResourceCacheTime()
+ */
+ @InitParameterName(Constants.SERVLET_PARAMETER_RESOURCE_CACHE_TIME)
+ public int resourceCacheTime() default DefaultDeploymentConfiguration.DEFAULT_RESOURCE_CACHE_TIME;
+
+ /**
+ * The number of seconds between heartbeat requests of a UI, or a
+ * non-positive number if heartbeat is disabled. The default value is 300
+ * seconds, i.e. 5 minutes.
+ *
+ * @return the time between heartbeats
+ *
+ * @see DeploymentConfiguration#getHeartbeatInterval()
+ */
+ @InitParameterName(Constants.SERVLET_PARAMETER_HEARTBEAT_INTERVAL)
+ public int heartbeatInterval() default DefaultDeploymentConfiguration.DEFAULT_HEARTBEAT_INTERVAL;
+
+ /**
+ * Whether a session should be closed when all its open UIs have been idle
+ * for longer than its configured maximum inactivity time. The default value
+ * is <code>false</code>.
+ *
+ * @return true if UIs and sessions receiving only heartbeat requests are
+ * eventually closed; false if heartbeat requests extend UI and
+ * session lifetime indefinitely
+ *
+ * @see DeploymentConfiguration#isCloseIdleSessions()
+ */
+ @InitParameterName(Constants.SERVLET_PARAMETER_CLOSE_IDLE_SESSIONS)
+ public boolean closeIdleSessions() default DefaultDeploymentConfiguration.DEFAULT_CLOSE_IDLE_SESSIONS;
+
+ /**
+ * The default widgetset to use for the servlet. The default value is
+ * <code>com.vaadin.DefaultWidgetSet</code>.
+ *
+ * @return the default widgetset name
+ */
+ @InitParameterName(VaadinServlet.PARAMETER_WIDGETSET)
+ public String widgetset() default VaadinServlet.DEFAULT_WIDGETSET;
+
+ /**
+ * The legacy Property.toString() mode used. The default value is
+ * {@link LegacyProperyToStringMode#DISABLED}
+ *
+ * @return The Property.toString() mode in use.
+ *
+ * @deprecated as of 7.1, should only be used to ease migration
+ */
+ @Deprecated
+ @InitParameterName(Constants.SERVLET_PARAMETER_LEGACY_PROPERTY_TOSTRING)
+ public LegacyProperyToStringMode legacyPropertyToStringMode() default LegacyProperyToStringMode.DISABLED;
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.annotations;
+
+import java.lang.annotation.ElementType;
++import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.vaadin.ui.UI;
+
+/**
+ * Defines a specific widgetset for a {@link UI}.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
++@Inherited
+public @interface Widgetset {
+ /**
+ * @return name of the widgetset
+ */
+ public String value();
+
+}
--- /dev/null
- * on the target class, its super classes and implemented interfaces are
- * also searched for the annotation.
+/*
+ * Copyright 2000-2014 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.server;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
++import java.lang.annotation.Inherited;
+
+import com.vaadin.annotations.PreserveOnRefresh;
+import com.vaadin.annotations.Push;
+import com.vaadin.annotations.Theme;
+import com.vaadin.annotations.Title;
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.shared.communication.PushMode;
+import com.vaadin.shared.ui.ui.Transport;
+import com.vaadin.ui.UI;
+
+public abstract class UIProvider implements Serializable {
+ public abstract Class<? extends UI> getUIClass(UIClassSelectionEvent event);
+
+ public UI createInstance(UICreateEvent event) {
+ try {
+ return event.getUIClass().newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Could not instantiate UI class", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Could not access UI class", e);
+ }
+ }
+
+ /**
+ * Helper to get an annotation for a class. If the annotation is not present
- // Find from the class hierarchy
- Class<?> currentType = clazz;
- while (currentType != Object.class) {
- T annotation = currentType.getAnnotation(annotationType);
++ * on the target class, its super classes and directly implemented
++ * interfaces are also searched for the annotation. Interfaces implemented
++ * by superclasses are not taken into account.
++ * <p>
++ * Note that searching implemented interfaces for {@code @Inherited}
++ * annotations and searching for superclasses for non-inherited annotations
++ * do not follow the standard semantics and are supported for backwards
++ * compatibility. Future versions of the framework might only support the
++ * standard semantics of {@code @Inherited}.
+ *
+ * @param clazz
+ * the class from which the annotation should be found
+ * @param annotationType
+ * the annotation type to look for
+ * @return an annotation of the given type, or <code>null</code> if the
+ * annotation is not present on the class
+ */
+ protected static <T extends Annotation> T getAnnotationFor(Class<?> clazz,
+ Class<T> annotationType) {
- } else {
- currentType = currentType.getSuperclass();
++ // Don't discover hierarchy if annotation is inherited
++ if (annotationType.getAnnotation(Inherited.class) != null) {
++ T annotation = clazz.getAnnotation(annotationType);
+ if (annotation != null) {
+ return annotation;
- // Find from an implemented interface
++ }
++ } else {
++ // Find from the class hierarchy
++ Class<?> currentType = clazz;
++ while (currentType != Object.class) {
++ T annotation = currentType.getAnnotation(annotationType);
++ if (annotation != null) {
++ return annotation;
++ } else {
++ currentType = currentType.getSuperclass();
++ }
+ }
+ }
+
++ // Find from a directly implemented interface
+ for (Class<?> iface : clazz.getInterfaces()) {
+ T annotation = iface.getAnnotation(annotationType);
+ if (annotation != null) {
+ return annotation;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the theme to use for a specific UI. If no specific theme is
+ * required, <code>null</code> is returned.
+ * <p>
+ * The default implementation checks for a @{@link Theme} annotation on the
+ * UI class.
+ *
+ * @param event
+ * the UI create event with information about the UI and the
+ * current request.
+ * @return the name of the theme, or <code>null</code> if the default theme
+ * should be used
+ *
+ */
+ public String getTheme(UICreateEvent event) {
+ Theme uiTheme = getAnnotationFor(event.getUIClass(), Theme.class);
+ if (uiTheme != null) {
+ return uiTheme.value();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Finds the widgetset to use for a specific UI. If no specific widgetset is
+ * required, <code>null</code> is returned.
+ * <p>
+ * The default implementation uses the @{@link Widgetset} annotation if it's
+ * defined for the UI class.
+ *
+ * @param event
+ * the UI create event with information about the UI and the
+ * current request.
+ * @return the name of the widgetset, or <code>null</code> if the default
+ * widgetset should be used
+ *
+ */
+ public String getWidgetset(UICreateEvent event) {
+ Widgetset uiWidgetset = getAnnotationFor(event.getUIClass(),
+ Widgetset.class);
+ if (uiWidgetset != null) {
+ return uiWidgetset.value();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Checks whether the same UI state should be reused if the framework can
+ * detect that the application is opened in a browser window where it has
+ * previously been open. The framework attempts to discover this by checking
+ * the value of window.name in the browser.
+ * <p>
+ * Whenever a preserved UI is reused, its
+ * {@link UI#refresh(com.vaadin.server.VaadinRequest) refresh} method is
+ * invoked by the framework first.
+ *
+ *
+ * @param event
+ * the UI create event with information about the UI and the
+ * current request.
+ *
+ * @return <code>true</code>if the same UI instance should be reused e.g.
+ * when the browser window is refreshed.
+ */
+ public boolean isPreservedOnRefresh(UICreateEvent event) {
+ PreserveOnRefresh preserveOnRefresh = getAnnotationFor(
+ event.getUIClass(), PreserveOnRefresh.class);
+ return preserveOnRefresh != null;
+ }
+
+ public String getPageTitle(UICreateEvent event) {
+ Title titleAnnotation = getAnnotationFor(event.getUIClass(),
+ Title.class);
+ if (titleAnnotation == null) {
+ return null;
+ } else {
+ return titleAnnotation.value();
+ }
+ }
+
+ /**
+ * Finds the {@link PushMode} to use for a specific UI. If no specific push
+ * mode is required, <code>null</code> is returned.
+ * <p>
+ * The default implementation uses the @{@link Push} annotation if it's
+ * defined for the UI class.
+ *
+ * @param event
+ * the UI create event with information about the UI and the
+ * current request.
+ * @return the push mode to use, or <code>null</code> if the default push
+ * mode should be used
+ *
+ */
+ public PushMode getPushMode(UICreateEvent event) {
+ Push push = getAnnotationFor(event.getUIClass(), Push.class);
+ if (push == null) {
+ return null;
+ } else {
+ return push.value();
+ }
+ }
+
+ /**
+ * Finds the {@link Transport} to use for a specific UI. If no transport is
+ * defined, <code>null</code> is returned.
+ * <p>
+ * The default implementation uses the @{@link Push} annotation if it's
+ * defined for the UI class.
+ *
+ * @param event
+ * the UI create event with information about the UI and the
+ * current request.
+ * @return the transport type to use, or <code>null</code> if the default
+ * transport type should be used
+ */
+ public Transport getPushTransport(UICreateEvent event) {
+ Push push = getAnnotationFor(event.getUIClass(), Push.class);
+ if (push == null) {
+ return null;
+ } else {
+ return push.transport();
+ }
+ }
+
+}
--- /dev/null
- "com.liferay.portal.util.PortalUtil",
- "getHttpServletRequest", request,
+/*
+ * Copyright 2000-2014 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.server;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.EventRequest;
+import javax.portlet.EventResponse;
+import javax.portlet.GenericPortlet;
+import javax.portlet.PortalContext;
+import javax.portlet.PortletConfig;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletException;
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletResponse;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+import javax.portlet.ResourceRequest;
+import javax.portlet.ResourceResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+import com.liferay.portal.kernel.util.PortalClassLoaderUtil;
+import com.liferay.portal.kernel.util.PropsUtil;
+import com.vaadin.server.communication.PortletDummyRequestHandler;
+import com.vaadin.server.communication.PortletUIInitHandler;
+import com.vaadin.util.CurrentInstance;
+
+/**
+ * Portlet 2.0 base class. This replaces the servlet in servlet/portlet 1.0
+ * deployments and handles various portlet requests from the browser.
+ *
+ * @author Vaadin Ltd
+ */
+public class VaadinPortlet extends GenericPortlet implements Constants,
+ Serializable {
+
+ /**
+ * Base class for portlet requests that need access to HTTP servlet
+ * requests.
+ */
+ public static abstract class VaadinHttpAndPortletRequest extends
+ VaadinPortletRequest {
+
+ /**
+ * Constructs a new {@link VaadinHttpAndPortletRequest}.
+ *
+ * @since 7.2
+ * @param request
+ * {@link PortletRequest} to be wrapped
+ * @param vaadinService
+ * {@link VaadinPortletService} associated with this request
+ */
+ public VaadinHttpAndPortletRequest(PortletRequest request,
+ VaadinPortletService vaadinService) {
+ super(request, vaadinService);
+ }
+
+ private HttpServletRequest originalRequest;
+
+ /**
+ * Returns the original HTTP servlet request for this portlet request.
+ *
+ * @since 7.2
+ * @param request
+ * {@link PortletRequest} used to
+ * @return the original HTTP servlet request
+ */
+ protected abstract HttpServletRequest getServletRequest(
+ PortletRequest request);
+
+ private HttpServletRequest getOriginalRequest() {
+ if (originalRequest == null) {
+ PortletRequest request = getRequest();
+ originalRequest = getServletRequest(request);
+ }
+
+ return originalRequest;
+ }
+
+ @Override
+ public String getParameter(String name) {
+ String parameter = super.getParameter(name);
+ if (parameter == null && getOriginalRequest() != null) {
+ parameter = getOriginalRequest().getParameter(name);
+ }
+ return parameter;
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ if (getOriginalRequest() != null) {
+ return getOriginalRequest().getRemoteAddr();
+ } else {
+ return super.getRemoteAddr();
+ }
+
+ }
+
+ @Override
+ public String getRemoteHost() {
+ if (getOriginalRequest() != null) {
+ return getOriginalRequest().getRemoteHost();
+ } else {
+ return super.getRemoteHost();
+ }
+ }
+
+ @Override
+ public int getRemotePort() {
+ if (getOriginalRequest() != null) {
+ return getOriginalRequest().getRemotePort();
+ } else {
+ return super.getRemotePort();
+ }
+ }
+
+ @Override
+ public String getHeader(String name) {
+ String header = super.getHeader(name);
+ if (header == null && getOriginalRequest() != null) {
+ header = getOriginalRequest().getHeader(name);
+ }
+ return header;
+ }
+
+ @Override
+ public Enumeration<String> getHeaderNames() {
+ Enumeration<String> headerNames = super.getHeaderNames();
+ if (headerNames == null && getOriginalRequest() != null) {
+ headerNames = getOriginalRequest().getHeaderNames();
+ }
+ return headerNames;
+ }
+
+ @Override
+ public Enumeration<String> getHeaders(String name) {
+ Enumeration<String> headers = super.getHeaders(name);
+ if (headers == null && getOriginalRequest() != null) {
+ headers = getOriginalRequest().getHeaders(name);
+ }
+ return headers;
+ }
+
+ @Override
+ public Map<String, String[]> getParameterMap() {
+ Map<String, String[]> parameterMap = super.getParameterMap();
+ if (parameterMap == null && getOriginalRequest() != null) {
+ parameterMap = getOriginalRequest().getParameterMap();
+ }
+ return parameterMap;
+ }
+ }
+
+ /**
+ * Portlet request for Liferay.
+ */
+ public static class VaadinLiferayRequest extends
+ VaadinHttpAndPortletRequest {
++ /**
++ * The PortalUtil class to use. Set to either
++ * {@link #LIFERAY_6_PORTAL_UTIL} or {@link #LIFERAY_7_PORTAL_UTIL} the
++ * first time it is needed.
++ */
++ private static String portalUtilClass = null;
++ private static final String LIFERAY_6_PORTAL_UTIL = "com.liferay.portal.util.PortalUtil";
++ private static final String LIFERAY_7_PORTAL_UTIL = "com.liferay.portal.kernel.util.PortalUtil";
+
+ public VaadinLiferayRequest(PortletRequest request,
+ VaadinPortletService vaadinService) {
+ super(request, vaadinService);
+ }
+
+ @Override
+ public String getPortalProperty(String name) {
+ return PropsUtil.get(name);
+ }
+
+ /**
+ * Simplified version of what Liferay PortalClassInvoker did. This is
+ * used because the API of PortalClassInvoker has changed in Liferay
+ * 6.2.
+ *
+ * This simply uses reflection with Liferay class loader. Parameters are
+ * Strings to avoid static dependencies and to load all classes with
+ * Liferay's own class loader. Only static utility methods are
+ * supported.
+ *
+ * This method is for internal use only and may change in future
+ * versions.
+ *
+ * @param className
+ * name of the Liferay class to call
+ * @param methodName
+ * name of the method to call
+ * @param parameterClassName
+ * name of the parameter class of the method
+ * @throws Exception
+ * @return return value of the invoked method
+ */
+ private Object invokeStaticLiferayMethod(String className,
+ String methodName, Object argument, String parameterClassName)
+ throws Exception {
+ Thread currentThread = Thread.currentThread();
+
+ ClassLoader contextClassLoader = currentThread
+ .getContextClassLoader();
+
+ try {
+ // this should be available across all Liferay versions with no
+ // problematic static dependencies
+ ClassLoader portalClassLoader = PortalClassLoaderUtil
+ .getClassLoader();
+ // this is in case the class loading triggers code that
+ // explicitly
+ // uses current thread class loader
+ currentThread.setContextClassLoader(portalClassLoader);
+
+ Class<?> targetClass = portalClassLoader.loadClass(className);
+ Class<?> parameterClass = portalClassLoader
+ .loadClass(parameterClassName);
+ Method method = targetClass.getMethod(methodName,
+ parameterClass);
+
+ return method.invoke(null, new Object[] { argument });
+ } catch (InvocationTargetException ite) {
+ throw (Exception) ite.getCause();
+ } finally {
+ currentThread.setContextClassLoader(contextClassLoader);
+ }
+ }
+
+ @Override
+ protected HttpServletRequest getServletRequest(PortletRequest request) {
++ if (portalUtilClass == null) {
++ try {
++ invokeStaticLiferayMethod(LIFERAY_7_PORTAL_UTIL,
++ "getHttpServletRequest", request,
++ "javax.portlet.PortletRequest");
++ portalUtilClass = LIFERAY_7_PORTAL_UTIL;
++ } catch (Exception e) {
++ // Liferay 6 or older
++ portalUtilClass = LIFERAY_6_PORTAL_UTIL;
++ }
++ }
+ try {
+ // httpRequest = PortalUtil.getHttpServletRequest(request);
+ HttpServletRequest httpRequest = (HttpServletRequest) invokeStaticLiferayMethod(
- "com.liferay.portal.util.PortalUtil",
- "getOriginalServletRequest", httpRequest,
- "javax.servlet.http.HttpServletRequest");
++ portalUtilClass, "getHttpServletRequest", request,
+ "javax.portlet.PortletRequest");
+
+ // httpRequest =
+ // PortalUtil.getOriginalServletRequest(httpRequest);
+ httpRequest = (HttpServletRequest) invokeStaticLiferayMethod(
++ portalUtilClass, "getOriginalServletRequest",
++ httpRequest, "javax.servlet.http.HttpServletRequest");
+ return httpRequest;
+ } catch (Exception e) {
+ throw new IllegalStateException("Liferay request not detected",
+ e);
+ }
+ }
+ }
+
+ /**
+ * Portlet request for GateIn.
+ */
+ public static class VaadinGateInRequest extends VaadinHttpAndPortletRequest {
+ public VaadinGateInRequest(PortletRequest request,
+ VaadinPortletService vaadinService) {
+ super(request, vaadinService);
+ }
+
+ @Override
+ protected HttpServletRequest getServletRequest(PortletRequest request) {
+ try {
+ Method getRealReq = request.getClass().getMethod(
+ "getRealRequest");
+ HttpServletRequestWrapper origRequest = (HttpServletRequestWrapper) getRealReq
+ .invoke(request);
+ return origRequest;
+ } catch (Exception e) {
+ throw new IllegalStateException("GateIn request not detected",
+ e);
+ }
+ }
+ }
+
+ /**
+ * Portlet request for WebSphere Portal.
+ */
+ public static class VaadinWebSpherePortalRequest extends
+ VaadinHttpAndPortletRequest {
+
+ public VaadinWebSpherePortalRequest(PortletRequest request,
+ VaadinPortletService vaadinService) {
+ super(request, vaadinService);
+ }
+
+ @Override
+ protected HttpServletRequest getServletRequest(PortletRequest request) {
+ try {
+ Class<?> portletUtils = Class
+ .forName("com.ibm.ws.portletcontainer.portlet.PortletUtils");
+ Method getHttpServletRequest = portletUtils.getMethod(
+ "getHttpServletRequest", PortletRequest.class);
+
+ return (HttpServletRequest) getHttpServletRequest.invoke(null,
+ request);
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ "WebSphere Portal request not detected.");
+ }
+ }
+ }
+
+ /**
+ * Portlet request for WebSphere Portal.
+ */
+ public static class VaadinWebLogicPortalRequest extends
+ VaadinHttpAndPortletRequest {
+ private static boolean warningLogged = false;
+
+ private static Method servletRequestMethod = null;
+
+ public VaadinWebLogicPortalRequest(PortletRequest request,
+ VaadinPortletService vaadinService) {
+ super(request, vaadinService);
+ }
+
+ @Override
+ protected HttpServletRequest getServletRequest(PortletRequest request) {
+ try {
+ if (servletRequestMethod == null) {
+ Class<?> portletRequestClass = Class
+ .forName("com.bea.portlet.container.PortletRequestImpl");
+ servletRequestMethod = portletRequestClass
+ .getDeclaredMethod("getInternalRequest",
+ new Class[] {});
+ servletRequestMethod.setAccessible(true);
+ }
+
+ return (HttpServletRequest) servletRequestMethod
+ .invoke(request);
+ } catch (Exception e) {
+ if (!warningLogged) {
+ warningLogged = true;
+ getLogger()
+ .log(Level.WARNING,
+ "Could not determine underlying servlet request for WebLogic Portal portlet request",
+ e);
+ }
+ return null;
+ }
+ }
+ }
+
+ /**
+ * @deprecated As of 7.0. Will likely change or be removed in a future
+ * version
+ */
+ @Deprecated
+ public static final String RESOURCE_URL_ID = "APP";
+
+ /**
+ * This portlet parameter is used to add styles to the main element. E.g
+ * "height:500px" generates a style="height:500px" to the main element.
+ *
+ * @deprecated As of 7.0. Will likely change or be removed in a future
+ * version
+ */
+ @Deprecated
+ public static final String PORTLET_PARAMETER_STYLE = "style";
+
+ /**
+ * This portal parameter is used to define the name of the Vaadin theme that
+ * is used for all Vaadin applications in the portal.
+ *
+ * @deprecated As of 7.0. Will likely change or be removed in a future
+ * version
+ */
+ @Deprecated
+ public static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme";
+
+ /**
+ * @deprecated As of 7.0. Will likely change or be removed in a future
+ * version
+ */
+ @Deprecated
+ public static final String WRITE_AJAX_PAGE_SCRIPT_WIDGETSET_SHOULD_WRITE = "writeAjaxPageScriptWidgetsetShouldWrite";
+
+ // TODO some parts could be shared with AbstractApplicationServlet
+
+ // TODO Can we close the application when the portlet is removed? Do we know
+ // when the portlet is removed?
+
+ private VaadinPortletService vaadinService;
+
+ @Override
+ public void init(PortletConfig config) throws PortletException {
+ CurrentInstance.clearAll();
+ super.init(config);
+ Properties initParameters = new Properties();
+
+ // Read default parameters from the context
+ final PortletContext context = config.getPortletContext();
+ for (final Enumeration<String> e = context.getInitParameterNames(); e
+ .hasMoreElements();) {
+ final String name = e.nextElement();
+ initParameters.setProperty(name, context.getInitParameter(name));
+ }
+
+ // Override with application settings from portlet.xml
+ for (final Enumeration<String> e = config.getInitParameterNames(); e
+ .hasMoreElements();) {
+ final String name = e.nextElement();
+ initParameters.setProperty(name, config.getInitParameter(name));
+ }
+
+ DeploymentConfiguration deploymentConfiguration = createDeploymentConfiguration(initParameters);
+ try {
+ vaadinService = createPortletService(deploymentConfiguration);
+ } catch (ServiceException e) {
+ throw new PortletException("Could not initialized VaadinPortlet", e);
+ }
+ // Sets current service even though there are no request and response
+ vaadinService.setCurrentInstances(null, null);
+
+ portletInitialized();
+
+ CurrentInstance.clearAll();
+ }
+
+ protected void portletInitialized() throws PortletException {
+
+ }
+
+ protected DeploymentConfiguration createDeploymentConfiguration(
+ Properties initParameters) {
+ return new DefaultDeploymentConfiguration(getClass(), initParameters);
+ }
+
+ protected VaadinPortletService createPortletService(
+ DeploymentConfiguration deploymentConfiguration)
+ throws ServiceException {
+ VaadinPortletService service = new VaadinPortletService(this,
+ deploymentConfiguration);
+ service.init();
+ return service;
+ }
+
+ /**
+ * @author Vaadin Ltd
+ *
+ * @deprecated As of 7.0. This is no longer used and only provided for
+ * backwards compatibility. Each {@link RequestHandler} can
+ * individually decide whether it wants to handle a request or
+ * not.
+ */
+ @Deprecated
+ protected enum RequestType {
+ FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APP, DUMMY, EVENT, ACTION, UNKNOWN, BROWSER_DETAILS, PUBLISHED_FILE, HEARTBEAT;
+ }
+
+ /**
+ * @param vaadinRequest
+ * @return
+ *
+ * @deprecated As of 7.0. This is no longer used and only provided for
+ * backwards compatibility. Each {@link RequestHandler} can
+ * individually decide whether it wants to handle a request or
+ * not.
+ */
+ @Deprecated
+ protected RequestType getRequestType(VaadinPortletRequest vaadinRequest) {
+ PortletRequest request = vaadinRequest.getPortletRequest();
+ if (request instanceof RenderRequest) {
+ return RequestType.RENDER;
+ } else if (request instanceof ResourceRequest) {
+ if (ServletPortletHelper.isUIDLRequest(vaadinRequest)) {
+ return RequestType.UIDL;
+ } else if (PortletUIInitHandler.isUIInitRequest(vaadinRequest)) {
+ return RequestType.BROWSER_DETAILS;
+ } else if (ServletPortletHelper.isFileUploadRequest(vaadinRequest)) {
+ return RequestType.FILE_UPLOAD;
+ } else if (ServletPortletHelper
+ .isPublishedFileRequest(vaadinRequest)) {
+ return RequestType.PUBLISHED_FILE;
+ } else if (ServletPortletHelper.isAppRequest(vaadinRequest)) {
+ return RequestType.APP;
+ } else if (ServletPortletHelper.isHeartbeatRequest(vaadinRequest)) {
+ return RequestType.HEARTBEAT;
+ } else if (PortletDummyRequestHandler.isDummyRequest(vaadinRequest)) {
+ return RequestType.DUMMY;
+ } else {
+ return RequestType.STATIC_FILE;
+ }
+ } else if (request instanceof ActionRequest) {
+ return RequestType.ACTION;
+ } else if (request instanceof EventRequest) {
+ return RequestType.EVENT;
+ }
+ return RequestType.UNKNOWN;
+ }
+
+ /**
+ * @param request
+ * @param response
+ * @throws PortletException
+ * @throws IOException
+ *
+ * @deprecated As of 7.0. Will likely change or be removed in a future
+ * version
+ */
+ @Deprecated
+ protected void handleRequest(PortletRequest request,
+ PortletResponse response) throws PortletException, IOException {
+
+ CurrentInstance.clearAll();
+ try {
+ getService().handleRequest(createVaadinRequest(request),
+ createVaadinResponse(response));
+ } catch (ServiceException e) {
+ throw new PortletException(e);
+ }
+ }
+
+ /**
+ * Wraps the request in a (possibly portal specific) Vaadin portlet request.
+ *
+ * @param request
+ * The original PortletRequest
+ * @return A wrapped version of the PortletRequest
+ */
+ protected VaadinPortletRequest createVaadinRequest(PortletRequest request) {
+ PortalContext portalContext = request.getPortalContext();
+ String portalInfo = portalContext.getPortalInfo().toLowerCase().trim();
+ VaadinPortletService service = getService();
+
+ if (portalInfo.contains("gatein")) {
+ return new VaadinGateInRequest(request, service);
+ }
+
+ if (portalInfo.contains("liferay")) {
+ return new VaadinLiferayRequest(request, service);
+ }
+
+ if (portalInfo.contains("websphere portal")) {
+ return new VaadinWebSpherePortalRequest(request, service);
+ }
+ if (portalInfo.contains("weblogic portal")) {
+ return new VaadinWebLogicPortalRequest(request, service);
+ }
+
+ return new VaadinPortletRequest(request, service);
+ }
+
+ private VaadinPortletResponse createVaadinResponse(PortletResponse response) {
+ return new VaadinPortletResponse(response, getService());
+ }
+
+ protected VaadinPortletService getService() {
+ return vaadinService;
+ }
+
+ @Override
+ public void processEvent(EventRequest request, EventResponse response)
+ throws PortletException, IOException {
+ handleRequest(request, response);
+ }
+
+ @Override
+ public void processAction(ActionRequest request, ActionResponse response)
+ throws PortletException, IOException {
+ handleRequest(request, response);
+ }
+
+ @Override
+ protected void doDispatch(RenderRequest request, RenderResponse response)
+ throws PortletException, IOException {
+ try {
+ // try to let super handle - it'll call methods annotated for
+ // handling, the default doXYZ(), or throw if a handler for the mode
+ // is not found
+ super.doDispatch(request, response);
+
+ } catch (PortletException e) {
+ if (e.getCause() == null) {
+ // No cause interpreted as 'unknown mode' - pass that trough
+ // so that the application can handle
+ handleRequest(request, response);
+
+ } else {
+ // Something else failed, pass on
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public void serveResource(ResourceRequest request, ResourceResponse response)
+ throws PortletException, IOException {
+ handleRequest(request, response);
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ getService().destroy();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(VaadinPortlet.class.getName());
+ }
+
+ /**
+ * Gets the currently used Vaadin portlet. The current portlet is
+ * automatically defined when initializing the portlet and when processing
+ * requests to the server and in threads started at a point when the current
+ * portlet is defined (see {@link InheritableThreadLocal}). In other cases,
+ * (e.g. from background threads started in some other way), the current
+ * portlet is not automatically defined.
+ * <p>
+ * The current portlet is derived from the current service using
+ * {@link VaadinService#getCurrent()}
+ *
+ * @return the current vaadin portlet instance if available, otherwise
+ * <code>null</code>
+ *
+ * @since 7.0
+ */
+ public static VaadinPortlet getCurrent() {
+ VaadinService vaadinService = CurrentInstance.get(VaadinService.class);
+ if (vaadinService instanceof VaadinPortletService) {
+ VaadinPortletService vps = (VaadinPortletService) vaadinService;
+ return vps.getPortlet();
+ } else {
+ return null;
+ }
+ }
+
+}
--- /dev/null
- * @param uI
+/*
+ * Copyright 2000-2014 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.server.communication;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Serializable;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.server.ClientConnector;
+import com.vaadin.server.Constants;
+import com.vaadin.server.JsonCodec;
+import com.vaadin.server.LegacyCommunicationManager;
+import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException;
+import com.vaadin.server.ServerRpcManager;
+import com.vaadin.server.ServerRpcManager.RpcInvocationException;
+import com.vaadin.server.ServerRpcMethodInvocation;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinService;
+import com.vaadin.server.VariableOwner;
+import com.vaadin.shared.ApplicationConstants;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.Version;
+import com.vaadin.shared.communication.LegacyChangeVariablesInvocation;
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.shared.communication.UidlValue;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ConnectorTracker;
+import com.vaadin.ui.UI;
+
+import elemental.json.JsonArray;
+import elemental.json.JsonException;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+import elemental.json.impl.JsonUtil;
+
+/**
+ * Handles a client-to-server message containing serialized {@link ServerRpc
+ * server RPC} invocations.
+ *
+ * @author Vaadin Ltd
+ * @since 7.1
+ */
+public class ServerRpcHandler implements Serializable {
+
+ /**
+ * A data transfer object representing an RPC request sent by the client
+ * side.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+ public static class RpcRequest implements Serializable {
+
+ private final String csrfToken;
+ private final JsonArray invocations;
+ private final int syncId;
+ private final JsonObject json;
+ private final boolean resynchronize;
+ private final int clientToServerMessageId;
+ private String widgetsetVersion = null;
+
+ public RpcRequest(String jsonString, VaadinRequest request) {
+ json = JsonUtil.parse(jsonString);
+
+ JsonValue token = json.get(ApplicationConstants.CSRF_TOKEN);
+ if (token == null) {
+ csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE;
+ } else {
+ String csrfToken = token.asString();
+ if (csrfToken.equals("")) {
+ csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE;
+ }
+ this.csrfToken = csrfToken;
+ }
+
+ if (request.getService().getDeploymentConfiguration()
+ .isSyncIdCheckEnabled()) {
+ syncId = (int) json
+ .getNumber(ApplicationConstants.SERVER_SYNC_ID);
+ } else {
+ syncId = -1;
+ }
+
+ if (json.hasKey(ApplicationConstants.RESYNCHRONIZE_ID)) {
+ resynchronize = json
+ .getBoolean(ApplicationConstants.RESYNCHRONIZE_ID);
+ } else {
+ resynchronize = false;
+ }
+ if (json.hasKey(ApplicationConstants.WIDGETSET_VERSION_ID)) {
+ widgetsetVersion = json
+ .getString(ApplicationConstants.WIDGETSET_VERSION_ID);
+ }
+
+ if (json.hasKey(ApplicationConstants.CLIENT_TO_SERVER_ID)) {
+ clientToServerMessageId = (int) json
+ .getNumber(ApplicationConstants.CLIENT_TO_SERVER_ID);
+ } else {
+ getLogger()
+ .warning("Server message without client id received");
+ clientToServerMessageId = -1;
+ }
+ invocations = json.getArray(ApplicationConstants.RPC_INVOCATIONS);
+ }
+
+ /**
+ * Gets the CSRF security token (double submit cookie) for this request.
+ *
+ * @return the CSRF security token for this current change request
+ */
+ public String getCsrfToken() {
+ return csrfToken;
+ }
+
+ /**
+ * Gets the data to recreate the RPC as requested by the client side.
+ *
+ * @return the data describing which RPC should be made, and all their
+ * data
+ */
+ public JsonArray getRpcInvocationsData() {
+ return invocations;
+ }
+
+ /**
+ * Gets the sync id last seen by the client.
+ *
+ * @return the last sync id given by the server, according to the
+ * client's request
+ */
+ public int getSyncId() {
+ return syncId;
+ }
+
+ /**
+ * Checks if this is a request to resynchronize the client side
+ *
+ * @return true if this is a resynchronization request, false otherwise
+ */
+ public boolean isResynchronize() {
+ return resynchronize;
+ }
+
+ /**
+ * Gets the id of the client to server message
+ *
+ * @since 7.6
+ * @return the server message id
+ */
+ public int getClientToServerId() {
+ return clientToServerMessageId;
+ }
+
+ /**
+ * Gets the entire request in JSON format, as it was received from the
+ * client.
+ * <p>
+ * <em>Note:</em> This is a shared reference - any modifications made
+ * will be shared.
+ *
+ * @return the raw JSON object that was received from the client
+ *
+ */
+ public JsonObject getRawJson() {
+ return json;
+ }
+
+ /**
+ * Gets the widget set version reported by the client
+ *
+ * @since 7.6
+ * @return The widget set version reported by the client or null if the
+ * message did not contain a widget set version
+ */
+ public String getWidgetsetVersion() {
+ return widgetsetVersion;
+ }
+ }
+
+ private static final int MAX_BUFFER_SIZE = 64 * 1024;
+
+ /**
+ * Reads JSON containing zero or more serialized RPC calls (including legacy
+ * variable changes) and executes the calls.
+ *
+ * @param ui
+ * The {@link UI} receiving the calls. Cannot be null.
+ * @param reader
+ * The {@link Reader} used to read the JSON.
+ * @param request
+ * @throws IOException
+ * If reading the message fails.
+ * @throws InvalidUIDLSecurityKeyException
+ * If the received security key does not match the one stored in
+ * the session.
+ */
+ public void handleRpc(UI ui, Reader reader, VaadinRequest request)
+ throws IOException, InvalidUIDLSecurityKeyException {
+ ui.getSession().setLastRequestTimestamp(System.currentTimeMillis());
+
+ String changeMessage = getMessage(reader);
+
+ if (changeMessage == null || changeMessage.equals("")) {
+ // The client sometimes sends empty messages, this is probably a bug
+ return;
+ }
+
+ RpcRequest rpcRequest = new RpcRequest(changeMessage, request);
+
+ // Security: double cookie submission pattern unless disabled by
+ // property
+ if (!VaadinService.isCsrfTokenValid(ui.getSession(),
+ rpcRequest.getCsrfToken())) {
+ throw new InvalidUIDLSecurityKeyException("");
+ }
+
+ checkWidgetsetVersion(rpcRequest.getWidgetsetVersion());
+
+ int expectedId = ui.getLastProcessedClientToServerId() + 1;
+ if (rpcRequest.getClientToServerId() != -1
+ && rpcRequest.getClientToServerId() != expectedId) {
+ // Invalid message id, skip RPC processing but force a full
+ // re-synchronization of the client as it might have not received
+ // the previous response (e.g. due to a bad connection)
+
+ // Must resync also for duplicate messages because the server might
+ // have generated a response for the first message but the response
+ // did not reach the client. When the client re-sends the message,
+ // it would only get an empty response (because the dirty flags have
+ // been cleared on the server) and would be out of sync
+ ui.getSession().getCommunicationManager().repaintAll(ui);
+
+ if (rpcRequest.getClientToServerId() < expectedId) {
+ // Just a duplicate message due to a bad connection or similar
+ // It has already been handled by the server so it is safe to
+ // ignore
+ getLogger().fine(
+ "Ignoring old message from the client. Expected: "
+ + expectedId + ", got: "
+ + rpcRequest.getClientToServerId());
+ } else {
+ getLogger().warning(
+ "Unexpected message id from the client. Expected: "
+ + expectedId + ", got: "
+ + rpcRequest.getClientToServerId());
+ }
+ } else {
+ // Message id ok, process RPCs
+ ui.setLastProcessedClientToServerId(expectedId);
+ handleInvocations(ui, rpcRequest.getSyncId(),
+ rpcRequest.getRpcInvocationsData());
+ }
+
+ ui.getConnectorTracker().cleanConcurrentlyRemovedConnectorIds(
+ rpcRequest.getSyncId());
+
+ if (rpcRequest.isResynchronize()) {
+ ui.getSession().getCommunicationManager().repaintAll(ui);
+ }
+
+ }
+
+ /**
+ * Checks that the version reported by the client (widgetset) matches that
+ * of the server.
+ *
+ * @param widgetsetVersion
+ * the widget set version reported by the client or null
+ */
+ private void checkWidgetsetVersion(String widgetsetVersion) {
+ if (widgetsetVersion == null) {
+ // Only check when the widgetset version is reported. It is reported
+ // in the first UIDL request (not the initial request as it is a
+ // plain GET /)
+ return;
+ }
+
+ if (!Version.getFullVersion().equals(widgetsetVersion)) {
+ getLogger().warning(
+ String.format(Constants.WIDGETSET_MISMATCH_INFO,
+ Version.getFullVersion(), widgetsetVersion));
+ }
+ }
+
+ /**
+ * Processes invocations data received from the client.
+ * <p>
+ * The invocations data can contain any number of RPC calls, including
+ * legacy variable change calls that are processed separately.
+ * <p>
+ * Consecutive changes to the value of the same variable are combined and
+ * changeVariables() is only called once for them. This preserves the Vaadin
+ * 6 semantics for components and add-ons that do not use Vaadin 7 RPC
+ * directly.
+ *
- private void handleInvocations(UI uI, int lastSyncIdSeenByClient,
++ * @param ui
+ * the UI receiving the invocations data
+ * @param lastSyncIdSeenByClient
+ * the most recent sync id the client has seen at the time the
+ * request was sent
+ * @param invocationsData
+ * JSON containing all information needed to execute all
+ * requested RPC calls.
++ * @since
+ */
- LegacyCommunicationManager manager = uI.getSession()
++ protected void handleInvocations(UI ui, int lastSyncIdSeenByClient,
+ JsonArray invocationsData) {
+ // TODO PUSH Refactor so that this is not needed
- ConnectorTracker connectorTracker = uI.getConnectorTracker();
++ LegacyCommunicationManager manager = ui.getSession()
+ .getCommunicationManager();
+
+ try {
- uI.getConnectorTracker(), invocationsData,
++ ConnectorTracker connectorTracker = ui.getConnectorTracker();
+
+ Set<Connector> enabledConnectors = new HashSet<Connector>();
+
+ List<MethodInvocation> invocations = parseInvocations(
- try {
- ServerRpcManager.applyInvocation(connector,
- (ServerRpcMethodInvocation) invocation);
- } catch (RpcInvocationException e) {
- manager.handleConnectorRelatedException(connector, e);
- }
++ ui.getConnectorTracker(), invocationsData,
+ lastSyncIdSeenByClient);
+ for (MethodInvocation invocation : invocations) {
+ final ClientConnector connector = connectorTracker
+ .getConnector(invocation.getConnectorId());
+
+ if (connector != null && connector.isConnectorEnabled()) {
+ enabledConnectors.add(connector);
+ }
+ }
+
+ for (int i = 0; i < invocations.size(); i++) {
+ MethodInvocation invocation = invocations.get(i);
+
+ final ClientConnector connector = connectorTracker
+ .getConnector(invocation.getConnectorId());
+ if (connector == null) {
+ getLogger()
+ .log(Level.WARNING,
+ "Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})",
+ new Object[] { invocation.getConnectorId(),
+ invocation.getInterfaceName(),
+ invocation.getMethodName() });
+ continue;
+ }
+
+ if (!enabledConnectors.contains(connector)) {
+
+ if (invocation instanceof LegacyChangeVariablesInvocation) {
+ LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
+ // TODO convert window close to a separate RPC call and
+ // handle above - not a variable change
+
+ // Handle special case where window-close is called
+ // after the window has been removed from the
+ // application or the application has closed
+ Map<String, Object> changes = legacyInvocation
+ .getVariableChanges();
+ if (changes.size() == 1 && changes.containsKey("close")
+ && Boolean.TRUE.equals(changes.get("close"))) {
+ // Silently ignore this
+ continue;
+ }
+ }
+
+ // Connector is disabled, log a warning and move to the next
+ getLogger().warning(
+ getIgnoredDisabledError("RPC call", connector));
+ continue;
+ }
+ // DragAndDropService has null UI
+ if (connector.getUI() != null && connector.getUI().isClosing()) {
+ String msg = "Ignoring RPC call for connector "
+ + connector.getClass().getName();
+ if (connector instanceof Component) {
+ String caption = ((Component) connector).getCaption();
+ if (caption != null) {
+ msg += ", caption=" + caption;
+ }
+ }
+ msg += " in closed UI";
+ getLogger().warning(msg);
+ continue;
+
+ }
+
+ if (invocation instanceof ServerRpcMethodInvocation) {
-
- // All code below is for legacy variable changes
++ handleInvocation(ui, connector,
++ (ServerRpcMethodInvocation) invocation);
+ } else {
- Map<String, Object> changes = legacyInvocation
- .getVariableChanges();
- try {
- if (connector instanceof VariableOwner) {
- // The source parameter is never used anywhere
- changeVariables(null, (VariableOwner) connector,
- changes);
- } else {
- throw new IllegalStateException(
- "Received legacy variable change for "
- + connector.getClass().getName()
- + " ("
- + connector.getConnectorId()
- + ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: "
- + changes.keySet());
- }
- } catch (Exception e) {
- manager.handleConnectorRelatedException(connector, e);
- }
+ LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
++ handleInvocation(ui, connector, legacyInvocation);
+ }
+ }
+ } catch (JsonException e) {
+ getLogger().warning(
+ "Unable to parse RPC call from the client: "
+ + e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
++ /**
++ * Handles the given RPC method invocation for the given connector
++ *
++ * @since
++ * @param ui
++ * the UI containing the connector
++ * @param connector
++ * the connector the RPC is targeted to
++ * @param invocation
++ * information about the rpc to invoke
++ */
++ protected void handleInvocation(UI ui, ClientConnector connector,
++ ServerRpcMethodInvocation invocation) {
++ try {
++ ServerRpcManager.applyInvocation(connector, invocation);
++ } catch (RpcInvocationException e) {
++ ui.getSession().getCommunicationManager()
++ .handleConnectorRelatedException(connector, e);
++ }
++
++ }
++
++ /**
++ * Handles the given Legacy variable change RPC method invocation for the
++ * given connector
++ *
++ * @since
++ * @param ui
++ * the UI containing the connector
++ * @param connector
++ * the connector the RPC is targeted to
++ * @param invocation
++ * information about the rpc to invoke
++ */
++ protected void handleInvocation(UI ui, ClientConnector connector,
++ LegacyChangeVariablesInvocation legacyInvocation) {
++ Map<String, Object> changes = legacyInvocation.getVariableChanges();
++ try {
++ if (connector instanceof VariableOwner) {
++ // The source parameter is never used anywhere
++ changeVariables(null, (VariableOwner) connector, changes);
++ } else {
++ throw new IllegalStateException(
++ "Received legacy variable change for "
++ + connector.getClass().getName()
++ + " ("
++ + connector.getConnectorId()
++ + ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: "
++ + changes.keySet());
++ }
++ } catch (Exception e) {
++ ui.getSession().getCommunicationManager()
++ .handleConnectorRelatedException(connector, e);
++ }
++
++ }
++
+ /**
+ * Parse JSON from the client into a list of MethodInvocation instances.
+ *
+ * @param connectorTracker
+ * The ConnectorTracker used to lookup connectors
+ * @param invocationsJson
+ * JSON containing all information needed to execute all
+ * requested RPC calls.
+ * @param lastSyncIdSeenByClient
+ * the most recent sync id the client has seen at the time the
+ * request was sent
+ * @return list of MethodInvocation to perform
+ */
+ private List<MethodInvocation> parseInvocations(
+ ConnectorTracker connectorTracker, JsonArray invocationsJson,
+ int lastSyncIdSeenByClient) {
+ int invocationCount = invocationsJson.length();
+ ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(
+ invocationCount);
+
+ MethodInvocation previousInvocation = null;
+ // parse JSON to MethodInvocations
+ for (int i = 0; i < invocationCount; ++i) {
+
+ JsonArray invocationJson = invocationsJson.getArray(i);
+
+ MethodInvocation invocation = parseInvocation(invocationJson,
+ previousInvocation, connectorTracker,
+ lastSyncIdSeenByClient);
+ if (invocation != null) {
+ // Can be null if the invocation was a legacy invocation and it
+ // was merged with the previous one or if the invocation was
+ // rejected because of an error.
+ invocations.add(invocation);
+ previousInvocation = invocation;
+ }
+ }
+ return invocations;
+ }
+
+ private MethodInvocation parseInvocation(JsonArray invocationJson,
+ MethodInvocation previousInvocation,
+ ConnectorTracker connectorTracker, long lastSyncIdSeenByClient) {
+ String connectorId = invocationJson.getString(0);
+ String interfaceName = invocationJson.getString(1);
+ String methodName = invocationJson.getString(2);
+
+ if (connectorTracker.getConnector(connectorId) == null
+ && !connectorId
+ .equals(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID)) {
+
+ if (!connectorTracker.connectorWasPresentAsRequestWasSent(
+ connectorId, lastSyncIdSeenByClient)) {
+ getLogger()
+ .log(Level.WARNING,
+ "RPC call to "
+ + interfaceName
+ + "."
+ + methodName
+ + " received for connector "
+ + connectorId
+ + " but no such connector could be found. Resynchronizing client.");
+ // This is likely an out of sync issue (client tries to update a
+ // connector which is not present). Force resync.
+ connectorTracker.markAllConnectorsDirty();
+ }
+ return null;
+ }
+
+ JsonArray parametersJson = invocationJson.getArray(3);
+
+ if (LegacyChangeVariablesInvocation.isLegacyVariableChange(
+ interfaceName, methodName)) {
+ if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) {
+ previousInvocation = null;
+ }
+
+ return parseLegacyChangeVariablesInvocation(connectorId,
+ interfaceName, methodName,
+ (LegacyChangeVariablesInvocation) previousInvocation,
+ parametersJson, connectorTracker);
+ } else {
+ return parseServerRpcInvocation(connectorId, interfaceName,
+ methodName, parametersJson, connectorTracker);
+ }
+
+ }
+
+ private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation(
+ String connectorId, String interfaceName, String methodName,
+ LegacyChangeVariablesInvocation previousInvocation,
+ JsonArray parametersJson, ConnectorTracker connectorTracker) {
+ if (parametersJson.length() != 2) {
+ throw new JsonException(
+ "Invalid parameters in legacy change variables call. Expected 2, was "
+ + parametersJson.length());
+ }
+ String variableName = parametersJson.getString(0);
+ UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType(
+ UidlValue.class, true, parametersJson.get(1), connectorTracker);
+
+ Object value = uidlValue.getValue();
+
+ if (previousInvocation != null
+ && previousInvocation.getConnectorId().equals(connectorId)) {
+ previousInvocation.setVariableChange(variableName, value);
+ return null;
+ } else {
+ return new LegacyChangeVariablesInvocation(connectorId,
+ variableName, value);
+ }
+ }
+
+ private ServerRpcMethodInvocation parseServerRpcInvocation(
+ String connectorId, String interfaceName, String methodName,
+ JsonArray parametersJson, ConnectorTracker connectorTracker)
+ throws JsonException {
+ ClientConnector connector = connectorTracker.getConnector(connectorId);
+
+ ServerRpcManager<?> rpcManager = connector.getRpcManager(interfaceName);
+ if (rpcManager == null) {
+ /*
+ * Security: Don't even decode the json parameters if no RpcManager
+ * corresponding to the received method invocation has been
+ * registered.
+ */
+ getLogger().warning(
+ "Ignoring RPC call to " + interfaceName + "." + methodName
+ + " in connector " + connector.getClass().getName()
+ + "(" + connectorId
+ + ") as no RPC implementation is registered");
+ return null;
+ }
+
+ // Use interface from RpcManager instead of loading the class based on
+ // the string name to avoid problems with OSGi
+ Class<? extends ServerRpc> rpcInterface = rpcManager.getRpcInterface();
+
+ ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation(
+ connectorId, rpcInterface, methodName, parametersJson.length());
+
+ Object[] parameters = new Object[parametersJson.length()];
+ Type[] declaredRpcMethodParameterTypes = invocation.getMethod()
+ .getGenericParameterTypes();
+
+ for (int j = 0; j < parametersJson.length(); ++j) {
+ JsonValue parameterValue = parametersJson.get(j);
+ Type parameterType = declaredRpcMethodParameterTypes[j];
+ parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType,
+ parameterValue, connectorTracker);
+ }
+ invocation.setParameters(parameters);
+ return invocation;
+ }
+
+ protected void changeVariables(Object source, VariableOwner owner,
+ Map<String, Object> m) {
+ owner.changeVariables(source, m);
+ }
+
+ protected String getMessage(Reader reader) throws IOException {
+
+ StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
+ char[] buffer = new char[MAX_BUFFER_SIZE];
+
+ while (true) {
+ int read = reader.read(buffer);
+ if (read == -1) {
+ break;
+ }
+ sb.append(buffer, 0, read);
+ }
+
+ return sb.toString();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(ServerRpcHandler.class.getName());
+ }
+
+ /**
+ * Generates an error message when the client is trying to to something
+ * ('what') with a connector which is disabled or invisible.
+ *
+ * @since 7.1.8
+ * @param connector
+ * the connector which is disabled (or invisible)
+ * @return an error message
+ */
+ public static String getIgnoredDisabledError(String what,
+ ClientConnector connector) {
+ String msg = "Ignoring " + what + " for disabled connector "
+ + connector.getClass().getName();
+ if (connector instanceof Component) {
+ String caption = ((Component) connector).getCaption();
+ if (caption != null) {
+ msg += ", caption=" + caption;
+ }
+ }
+ return msg;
+ }
+}
--- /dev/null
- private ServerRpcHandler rpcHandler = new ServerRpcHandler();
+/*
+ * Copyright 2000-2014 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.server.communication;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException;
+import com.vaadin.server.ServletPortletHelper;
+import com.vaadin.server.SessionExpiredHandler;
+import com.vaadin.server.SynchronizedRequestHandler;
+import com.vaadin.server.SystemMessages;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinResponse;
+import com.vaadin.server.VaadinService;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.JsonConstants;
+import com.vaadin.ui.UI;
+
+import elemental.json.JsonException;
+
+/**
+ * Processes a UIDL request from the client.
+ *
+ * Uses {@link ServerRpcHandler} to execute client-to-server RPC invocations and
+ * {@link UidlWriter} to write state changes and client RPC calls back to the
+ * client.
+ *
+ * @author Vaadin Ltd
+ * @since 7.1
+ */
+public class UidlRequestHandler extends SynchronizedRequestHandler implements
+ SessionExpiredHandler {
+
+ public static final String UIDL_PATH = "UIDL/";
+
++ private ServerRpcHandler rpcHandler;
+
+ public UidlRequestHandler() {
++ rpcHandler = createRpcHandler();
++ }
++
++ /**
++ * Creates the ServerRpcHandler to use
++ *
++ * @since
++ * @return the ServerRpcHandler to use
++ */
++ protected ServerRpcHandler createRpcHandler() {
++ return new ServerRpcHandler();
+ }
+
+ @Override
+ protected boolean canHandleRequest(VaadinRequest request) {
+ return ServletPortletHelper.isUIDLRequest(request);
+ }
+
+ @Override
+ public boolean synchronizedHandleRequest(VaadinSession session,
+ VaadinRequest request, VaadinResponse response) throws IOException {
+ UI uI = session.getService().findUI(request);
+ if (uI == null) {
+ // This should not happen but it will if the UI has been closed. We
+ // really don't want to see it in the server logs though
+ UIInitHandler.commitJsonResponse(request, response,
+ getUINotFoundErrorJSON(session.getService(), request));
+ return true;
+ }
+
+ StringWriter stringWriter = new StringWriter();
+
+ try {
+ rpcHandler.handleRpc(uI, request.getReader(), request);
+
+ writeUidl(request, response, uI, stringWriter);
+ } catch (JsonException e) {
+ getLogger().log(Level.SEVERE, "Error writing JSON to response", e);
+ // Refresh on client side
+ writeRefresh(request, response);
+ return true;
+ } catch (InvalidUIDLSecurityKeyException e) {
+ getLogger().log(Level.WARNING,
+ "Invalid security key received from {0}",
+ request.getRemoteHost());
+ // Refresh on client side
+ writeRefresh(request, response);
+ return true;
+ } finally {
+ stringWriter.close();
+ }
+
+ return UIInitHandler.commitJsonResponse(request, response,
+ stringWriter.toString());
+ }
+
+ private void writeRefresh(VaadinRequest request, VaadinResponse response)
+ throws IOException {
+ String json = VaadinService.createCriticalNotificationJSON(null, null,
+ null, null);
+ UIInitHandler.commitJsonResponse(request, response, json);
+ }
+
+ private void writeUidl(VaadinRequest request, VaadinResponse response,
+ UI ui, Writer writer) throws IOException {
+ openJsonMessage(writer, response);
+
+ new UidlWriter().write(ui, writer, false);
+
+ closeJsonMessage(writer);
+ }
+
+ protected void closeJsonMessage(Writer outWriter) throws IOException {
+ outWriter.write("}]");
+ }
+
+ /**
+ * Writes the opening of JSON message to be sent to client.
+ *
+ * @param outWriter
+ * @param response
+ * @throws IOException
+ */
+ protected void openJsonMessage(Writer outWriter, VaadinResponse response)
+ throws IOException {
+ // some dirt to prevent cross site scripting
+ outWriter.write("for(;;);[{");
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(UidlRequestHandler.class.getName());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.server.SessionExpiredHandler#handleSessionExpired(com.vaadin
+ * .server.VaadinRequest, com.vaadin.server.VaadinResponse)
+ */
+ @Override
+ public boolean handleSessionExpired(VaadinRequest request,
+ VaadinResponse response) throws IOException {
+ if (!ServletPortletHelper.isUIDLRequest(request)) {
+ return false;
+ }
+ VaadinService service = request.getService();
+ SystemMessages systemMessages = service.getSystemMessages(
+ ServletPortletHelper.findLocale(null, null, request), request);
+
+ service.writeStringResponse(response, JsonConstants.JSON_CONTENT_TYPE,
+ VaadinService.createCriticalNotificationJSON(
+ systemMessages.getSessionExpiredCaption(),
+ systemMessages.getSessionExpiredMessage(), null,
+ systemMessages.getSessionExpiredURL()));
+
+ return true;
+ }
+
+ /**
+ * Returns the JSON which should be returned to the client when a request
+ * for a non-existent UI arrives.
+ *
+ * @param service
+ * The VaadinService
+ * @param vaadinRequest
+ * The request which triggered this, or null if not available
+ * @since 7.1
+ * @return A JSON string
+ */
+ static String getUINotFoundErrorJSON(VaadinService service,
+ VaadinRequest vaadinRequest) {
+ SystemMessages ci = service.getSystemMessages(
+ vaadinRequest.getLocale(), vaadinRequest);
+ // Session Expired is not really the correct message as the
+ // session exists but the requested UI does not.
+ // Using Communication Error for now.
+ String json = VaadinService.createCriticalNotificationJSON(
+ ci.getCommunicationErrorCaption(),
+ ci.getCommunicationErrorMessage(), null,
+ ci.getCommunicationErrorURL());
+
+ return json;
+ }
+
+}
--- /dev/null
- writer.write(String.format(", \"timings\":[%d, %d]", ui.getSession()
- .getCumulativeRequestDuration(), ui.getSession()
- .getLastRequestDuration()));
+/*
+ * Copyright 2000-2014 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.server.communication;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.annotations.JavaScript;
+import com.vaadin.annotations.StyleSheet;
+import com.vaadin.server.ClientConnector;
+import com.vaadin.server.JsonPaintTarget;
+import com.vaadin.server.LegacyCommunicationManager;
+import com.vaadin.server.LegacyCommunicationManager.ClientCache;
+import com.vaadin.server.SystemMessages;
+import com.vaadin.server.VaadinService;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.ApplicationConstants;
+import com.vaadin.ui.ConnectorTracker;
+import com.vaadin.ui.UI;
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.impl.JsonUtil;
+
+/**
+ * Serializes pending server-side changes to UI state to JSON. This includes
+ * shared state, client RPC invocations, connector hierarchy changes, connector
+ * type information among others.
+ *
+ * @author Vaadin Ltd
+ * @since 7.1
+ */
+public class UidlWriter implements Serializable {
+
+ /**
+ * Writes a JSON object containing all pending changes to the given UI.
+ *
+ * @param ui
+ * The {@link UI} whose changes to write
+ * @param writer
+ * The writer to use
+ * @param analyzeLayouts
+ * Whether detected layout problems should be logged.
+ * @param async
+ * True if this message is sent by the server asynchronously,
+ * false if it is a response to a client message.
+ *
+ * @throws IOException
+ * If the writing fails.
+ */
+ public void write(UI ui, Writer writer, boolean async) throws IOException {
+ VaadinSession session = ui.getSession();
+ VaadinService service = session.getService();
+
+ // Purge pending access calls as they might produce additional changes
+ // to write out
+ service.runPendingAccessTasks(session);
+
+ Set<ClientConnector> processedConnectors = new HashSet<ClientConnector>();
+
+ LegacyCommunicationManager manager = session.getCommunicationManager();
+ ClientCache clientCache = manager.getClientCache(ui);
+ boolean repaintAll = clientCache.isEmpty();
+ // Paints components
+ ConnectorTracker uiConnectorTracker = ui.getConnectorTracker();
+ getLogger().log(Level.FINE, "* Creating response to client");
+
+ while (true) {
+ ArrayList<ClientConnector> connectorsToProcess = new ArrayList<ClientConnector>();
+ for (ClientConnector c : uiConnectorTracker.getDirtyConnectors()) {
+ if (!processedConnectors.contains(c)
+ && LegacyCommunicationManager
+ .isConnectorVisibleToClient(c)) {
+ connectorsToProcess.add(c);
+ }
+ }
+
+ if (connectorsToProcess.isEmpty()) {
+ break;
+ }
+
+ for (ClientConnector connector : connectorsToProcess) {
+ boolean initialized = uiConnectorTracker
+ .isClientSideInitialized(connector);
+ processedConnectors.add(connector);
+
+ try {
+ connector.beforeClientResponse(!initialized);
+ } catch (RuntimeException e) {
+ manager.handleConnectorRelatedException(connector, e);
+ }
+ }
+ }
+
+ getLogger().log(
+ Level.FINE,
+ "Found " + processedConnectors.size()
+ + " dirty connectors to paint");
+
+ uiConnectorTracker.setWritingResponse(true);
+ try {
+
+ int syncId = service.getDeploymentConfiguration()
+ .isSyncIdCheckEnabled() ? uiConnectorTracker
+ .getCurrentSyncId() : -1;
+ writer.write("\"" + ApplicationConstants.SERVER_SYNC_ID + "\": "
+ + syncId + ", ");
+ if (repaintAll) {
+ writer.write("\"" + ApplicationConstants.RESYNCHRONIZE_ID
+ + "\": true, ");
+ }
+ int nextClientToServerMessageId = ui
+ .getLastProcessedClientToServerId() + 1;
+ writer.write("\"" + ApplicationConstants.CLIENT_TO_SERVER_ID
+ + "\": " + nextClientToServerMessageId + ", ");
+ writer.write("\"changes\" : ");
+
+ JsonPaintTarget paintTarget = new JsonPaintTarget(manager, writer,
+ !repaintAll);
+
+ new LegacyUidlWriter().write(ui, writer, paintTarget);
+
+ paintTarget.close();
+ writer.write(", "); // close changes
+
+ // send shared state to client
+
+ // for now, send the complete state of all modified and new
+ // components
+
+ // Ideally, all this would be sent before "changes", but that causes
+ // complications with legacy components that create sub-components
+ // in their paint phase. Nevertheless, this will be processed on the
+ // client after component creation but before legacy UIDL
+ // processing.
+
+ writer.write("\"state\":");
+ Set<String> stateUpdateConnectors = new SharedStateWriter().write(
+ ui, writer);
+ writer.write(", "); // close states
+
+ // TODO This should be optimized. The type only needs to be
+ // sent once for each connector id + on refresh. Use the same cache
+ // as
+ // widget mapping
+
+ writer.write("\"types\":");
+ new ConnectorTypeWriter().write(ui, writer, paintTarget);
+ writer.write(", "); // close states
+
+ // Send update hierarchy information to the client.
+
+ // This could be optimized aswell to send only info if hierarchy has
+ // actually changed. Much like with the shared state. Note though
+ // that an empty hierarchy is information aswell (e.g. change from 1
+ // child to 0 children)
+
+ writer.write("\"hierarchy\":");
+ new ConnectorHierarchyWriter().write(ui, writer,
+ stateUpdateConnectors);
+ writer.write(", "); // close hierarchy
+
+ // send server to client RPC calls for components in the UI, in call
+ // order
+
+ // collect RPC calls from components in the UI in the order in
+ // which they were performed, remove the calls from components
+
+ writer.write("\"rpc\" : ");
+ new ClientRpcWriter().write(ui, writer);
+ writer.write(", "); // close rpc
+
+ uiConnectorTracker.markAllConnectorsClean();
+
+ writer.write("\"meta\" : ");
+
+ SystemMessages messages = ui.getSession().getService()
+ .getSystemMessages(ui.getLocale(), null);
+ // TODO hilightedConnector
+ new MetadataWriter().write(ui, writer, repaintAll, async, messages);
+ writer.write(", ");
+
+ writer.write("\"resources\" : ");
+ new ResourceWriter().write(ui, writer, paintTarget);
+
+ Collection<Class<? extends ClientConnector>> usedClientConnectors = paintTarget
+ .getUsedClientConnectors();
+ boolean typeMappingsOpen = false;
+
+ List<Class<? extends ClientConnector>> newConnectorTypes = new ArrayList<Class<? extends ClientConnector>>();
+
+ for (Class<? extends ClientConnector> class1 : usedClientConnectors) {
+ if (clientCache.cache(class1)) {
+ // client does not know the mapping key for this type, send
+ // mapping to client
+ newConnectorTypes.add(class1);
+
+ if (!typeMappingsOpen) {
+ typeMappingsOpen = true;
+ writer.write(", \"typeMappings\" : { ");
+ } else {
+ writer.write(" , ");
+ }
+ String canonicalName = class1.getCanonicalName();
+ writer.write("\"");
+ writer.write(canonicalName);
+ writer.write("\" : ");
+ writer.write(manager.getTagForType(class1));
+ }
+ }
+ if (typeMappingsOpen) {
+ writer.write(" }");
+ }
+
+ // TODO PUSH Refactor to TypeInheritanceWriter or something
+ boolean typeInheritanceMapOpen = false;
+ if (typeMappingsOpen) {
+ // send the whole type inheritance map if any new mappings
+ for (Class<? extends ClientConnector> class1 : usedClientConnectors) {
+ if (!ClientConnector.class.isAssignableFrom(class1
+ .getSuperclass())) {
+ continue;
+ }
+ if (!typeInheritanceMapOpen) {
+ typeInheritanceMapOpen = true;
+ writer.write(", \"typeInheritanceMap\" : { ");
+ } else {
+ writer.write(" , ");
+ }
+ writer.write("\"");
+ writer.write(manager.getTagForType(class1));
+ writer.write("\" : ");
+ writer.write(manager
+ .getTagForType((Class<? extends ClientConnector>) class1
+ .getSuperclass()));
+ }
+ if (typeInheritanceMapOpen) {
+ writer.write(" }");
+ }
+ }
+
+ // TODO Refactor to DependencyWriter or something
+ /*
+ * Ensure super classes come before sub classes to get script
+ * dependency order right. Sub class @JavaScript might assume that
+ *
+ * @JavaScript defined by super class is already loaded.
+ */
+ Collections.sort(newConnectorTypes, new Comparator<Class<?>>() {
+ @Override
+ public int compare(Class<?> o1, Class<?> o2) {
+ // TODO optimize using Class.isAssignableFrom?
+ return hierarchyDepth(o1) - hierarchyDepth(o2);
+ }
+
+ private int hierarchyDepth(Class<?> type) {
+ if (type == Object.class) {
+ return 0;
+ } else {
+ return hierarchyDepth(type.getSuperclass()) + 1;
+ }
+ }
+ });
+
+ List<String> scriptDependencies = new ArrayList<String>();
+ List<String> styleDependencies = new ArrayList<String>();
+
+ for (Class<? extends ClientConnector> class1 : newConnectorTypes) {
+ JavaScript jsAnnotation = class1
+ .getAnnotation(JavaScript.class);
+ if (jsAnnotation != null) {
+ for (String uri : jsAnnotation.value()) {
+ scriptDependencies.add(manager.registerDependency(uri,
+ class1));
+ }
+ }
+
+ StyleSheet styleAnnotation = class1
+ .getAnnotation(StyleSheet.class);
+ if (styleAnnotation != null) {
+ for (String uri : styleAnnotation.value()) {
+ styleDependencies.add(manager.registerDependency(uri,
+ class1));
+ }
+ }
+ }
+
+ // Include script dependencies in output if there are any
+ if (!scriptDependencies.isEmpty()) {
+ writer.write(", \"scriptDependencies\": "
+ + JsonUtil.stringify(toJsonArray(scriptDependencies)));
+ }
+
+ // Include style dependencies in output if there are any
+ if (!styleDependencies.isEmpty()) {
+ writer.write(", \"styleDependencies\": "
+ + JsonUtil.stringify(toJsonArray(styleDependencies)));
+ }
+
+ session.getDragAndDropService().printJSONResponse(writer);
+
+ for (ClientConnector connector : processedConnectors) {
+ uiConnectorTracker.markClientSideInitialized(connector);
+ }
+
+ assert (uiConnectorTracker.getDirtyConnectors().isEmpty()) : "Connectors have been marked as dirty during the end of the paint phase. This is most certainly not intended.";
+
+ writePerformanceData(ui, writer);
+ } finally {
+ uiConnectorTracker.setWritingResponse(false);
+ uiConnectorTracker.cleanConnectorMap();
+ }
+ }
+
+ private JsonArray toJsonArray(List<String> list) {
+ JsonArray result = Json.createArray();
+ for (int i = 0; i < list.size(); i++) {
+ result.set(i, list.get(i));
+ }
+
+ return result;
+ }
+
+ /**
+ * Adds the performance timing data (used by TestBench 3) to the UIDL
+ * response.
+ *
+ * @throws IOException
+ */
+ private void writePerformanceData(UI ui, Writer writer) throws IOException {
++ if (!ui.getSession().getService().getDeploymentConfiguration()
++ .isProductionMode()) {
++ writer.write(String.format(", \"timings\":[%d, %d]", ui
++ .getSession().getCumulativeRequestDuration(), ui
++ .getSession().getLastRequestDuration()));
++ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(UidlWriter.class.getName());
+ }
+}
--- /dev/null
- return new URL("file://" + directory.getCanonicalPath());
- } catch (MalformedURLException e) {
- // ignore: continue to the next classpath entry
- if (debug) {
- e.printStackTrace();
+/*
+ * Copyright 2000-2014 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.server.widgetsetutils;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+/**
+ * Utility class to collect widgetset related information from classpath.
+ * Utility will seek all directories from classpaths, and jar files having
+ * "Vaadin-Widgetsets" key in their manifest file.
+ * <p>
+ * Used by WidgetMapGenerator and ide tools to implement some monkey coding for
+ * you.
+ * <p>
+ * Developer notice: If you end up reading this comment, I guess you have faced
+ * a sluggish performance of widget compilation or unreliable detection of
+ * components in your classpaths. The thing you might be able to do is to use
+ * annotation processing tool like apt to generate the needed information. Then
+ * either use that information in {@link WidgetMapGenerator} or create the
+ * appropriate monkey code for gwt directly in annotation processor and get rid
+ * of {@link WidgetMapGenerator}. Using annotation processor might be a good
+ * idea when dropping Java 1.5 support (integrated to javac in 6).
+ *
+ */
+public class ClassPathExplorer {
+
+ private static final String VAADIN_ADDON_VERSION_ATTRIBUTE = "Vaadin-Package-Version";
+
+ /**
+ * File filter that only accepts directories.
+ */
+ private final static FileFilter DIRECTORIES_ONLY = new FileFilter() {
+ @Override
+ public boolean accept(File f) {
+ if (f.exists() && f.isDirectory()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ /**
+ * Contains information about widgetsets and themes found on the classpath
+ *
+ * @since 7.1
+ */
+ public static class LocationInfo {
+
+ private final Map<String, URL> widgetsets;
+
+ private final Map<String, URL> addonStyles;
+
+ public LocationInfo(Map<String, URL> widgetsets, Map<String, URL> themes) {
+ this.widgetsets = widgetsets;
+ addonStyles = themes;
+ }
+
+ public Map<String, URL> getWidgetsets() {
+ return widgetsets;
+ }
+
+ public Map<String, URL> getAddonStyles() {
+ return addonStyles;
+ }
+
+ }
+
+ /**
+ * Raw class path entries as given in the java class path string. Only
+ * entries that could include widgets/widgetsets are listed (primarily
+ * directories, Vaadin JARs and add-on JARs).
+ */
+ private static List<String> rawClasspathEntries = getRawClasspathEntries();
+
+ /**
+ * Map from identifiers (either a package name preceded by the path and a
+ * slash, or a URL for a JAR file) to the corresponding URLs. This is
+ * constructed from the class path.
+ */
+ private static Map<String, URL> classpathLocations = getClasspathLocations(rawClasspathEntries);
+
+ private static boolean debug = false;
+
+ static {
+ String debugProperty = System.getProperty("debug");
+ if (debugProperty != null && !debugProperty.equals("")) {
+ debug = true;
+ }
+ }
+
+ /**
+ * No instantiation from outside, callable methods are static.
+ */
+ private ClassPathExplorer() {
+ }
+
+ /**
+ * Finds the names and locations of widgetsets available on the class path.
+ *
+ * @return map from widgetset classname to widgetset location URL
+ * @deprecated Use {@link #getAvailableWidgetSetsAndStylesheets()} instead
+ */
+ @Deprecated
+ public static Map<String, URL> getAvailableWidgetSets() {
+ return getAvailableWidgetSetsAndStylesheets().getWidgetsets();
+ }
+
+ /**
+ * Finds the names and locations of widgetsets and themes available on the
+ * class path.
+ *
+ * @return
+ */
+ public static LocationInfo getAvailableWidgetSetsAndStylesheets() {
+ long start = System.currentTimeMillis();
+ Map<String, URL> widgetsets = new HashMap<String, URL>();
+ Map<String, URL> themes = new HashMap<String, URL>();
+ Set<String> keySet = classpathLocations.keySet();
+ for (String location : keySet) {
+ searchForWidgetSetsAndAddonStyles(location, widgetsets, themes);
+ }
+ long end = System.currentTimeMillis();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Widgetsets found from classpath:\n");
+ for (String ws : widgetsets.keySet()) {
+ sb.append("\t");
+ sb.append(ws);
+ sb.append(" in ");
+ sb.append(widgetsets.get(ws));
+ sb.append("\n");
+ }
+
+ sb.append("Addon styles found from classpath:\n");
+ for (String theme : themes.keySet()) {
+ sb.append("\t");
+ sb.append(theme);
+ sb.append(" in ");
+ sb.append(themes.get(theme));
+ sb.append("\n");
+ }
+
+ log(sb.toString());
+ log("Search took " + (end - start) + "ms");
+ return new LocationInfo(widgetsets, themes);
+ }
+
+ /**
+ * Finds all GWT modules / Vaadin widgetsets and Addon styles in a valid
+ * location.
+ *
+ * If the location is a directory, all GWT modules (files with the
+ * ".gwt.xml" extension) are added to widgetsets.
+ *
+ * If the location is a JAR file, the comma-separated values of the
+ * "Vaadin-Widgetsets" attribute in its manifest are added to widgetsets.
+ *
+ * @param locationString
+ * an entry in {@link #classpathLocations}
+ * @param widgetsets
+ * a map from widgetset name (including package, with dots as
+ * separators) to a URL (see {@link #classpathLocations}) - new
+ * entries are added to this map
+ */
+ private static void searchForWidgetSetsAndAddonStyles(
+ String locationString, Map<String, URL> widgetsets,
+ Map<String, URL> addonStyles) {
+
+ URL location = classpathLocations.get(locationString);
+ File directory = new File(location.getFile());
+
+ if (directory.exists() && !directory.isHidden()) {
+ // Get the list of the files contained in the directory
+ String[] files = directory.list();
+ for (int i = 0; i < files.length; i++) {
+ // we are only interested in .gwt.xml files
+ if (!files[i].endsWith(".gwt.xml")) {
+ continue;
+ }
+
+ // remove the .gwt.xml extension
+ String classname = files[i].substring(0, files[i].length() - 8);
+ String packageName = locationString.substring(locationString
+ .lastIndexOf("/") + 1);
+ classname = packageName + "." + classname;
+
+ if (!WidgetSetBuilder.isWidgetset(classname)) {
+ // Only return widgetsets and not GWT modules to avoid
+ // comparing modules and widgetsets
+ continue;
+ }
+
+ if (!widgetsets.containsKey(classname)) {
+ String packagePath = packageName.replaceAll("\\.", "/");
+ String basePath = location.getFile().replaceAll(
+ "/" + packagePath + "$", "");
+ try {
+ URL url = new URL(location.getProtocol(),
+ location.getHost(), location.getPort(),
+ basePath);
+ widgetsets.put(classname, url);
+ } catch (MalformedURLException e) {
+ // should never happen as based on an existing URL,
+ // only changing end of file name/path part
+ error("Error locating the widgetset " + classname, e);
+ }
+ }
+ }
+ } else {
+
+ try {
+ // check files in jar file, entries will list all directories
+ // and files in jar
+
+ URLConnection openConnection = location.openConnection();
+ if (openConnection instanceof JarURLConnection) {
+ JarURLConnection conn = (JarURLConnection) openConnection;
+
+ JarFile jarFile = conn.getJarFile();
+
+ Manifest manifest = jarFile.getManifest();
+ if (manifest == null) {
+ // No manifest so this is not a Vaadin Add-on
+ return;
+ }
+
+ // Check for widgetset attribute
+ String value = manifest.getMainAttributes().getValue(
+ "Vaadin-Widgetsets");
+ if (value != null) {
+ String[] widgetsetNames = value.split(",");
+ for (int i = 0; i < widgetsetNames.length; i++) {
+ String widgetsetname = widgetsetNames[i].trim();
+ if (!widgetsetname.equals("")) {
+ widgetsets.put(widgetsetname, location);
+ }
+ }
+ }
+
+ // Check for theme attribute
+ value = manifest.getMainAttributes().getValue(
+ "Vaadin-Stylesheets");
+ if (value != null) {
+ String[] stylesheets = value.split(",");
+ for (int i = 0; i < stylesheets.length; i++) {
+ String stylesheet = stylesheets[i].trim();
+ if (!stylesheet.equals("")) {
+ addonStyles.put(stylesheet, location);
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ error("Error parsing jar file", e);
+ }
+
+ }
+ }
+
+ /**
+ * Splits the current class path into entries, and filters them accepting
+ * directories, Vaadin add-on JARs with widgetsets and Vaadin JARs.
+ *
+ * Some other non-JAR entries may also be included in the result.
+ *
+ * @return filtered list of class path entries
+ */
+ private final static List<String> getRawClasspathEntries() {
+ // try to keep the order of the classpath
+ List<String> locations = new ArrayList<String>();
+
+ String pathSep = System.getProperty("path.separator");
+ String classpath = System.getProperty("java.class.path");
+
+ if (classpath.startsWith("\"")) {
+ classpath = classpath.substring(1);
+ }
+ if (classpath.endsWith("\"")) {
+ classpath = classpath.substring(0, classpath.length() - 1);
+ }
+
+ debug("Classpath: " + classpath);
+
+ String[] split = classpath.split(pathSep);
+ for (int i = 0; i < split.length; i++) {
+ String classpathEntry = split[i];
+ if (acceptClassPathEntry(classpathEntry)) {
+ locations.add(classpathEntry);
+ }
+ }
+
+ return locations;
+ }
+
+ /**
+ * Determine every URL location defined by the current classpath, and it's
+ * associated package name.
+ *
+ * See {@link #classpathLocations} for information on output format.
+ *
+ * @param rawClasspathEntries
+ * raw class path entries as split from the Java class path
+ * string
+ * @return map of classpath locations, see {@link #classpathLocations}
+ */
+ private final static Map<String, URL> getClasspathLocations(
+ List<String> rawClasspathEntries) {
+ long start = System.currentTimeMillis();
+ // try to keep the order of the classpath
+ Map<String, URL> locations = new LinkedHashMap<String, URL>();
+ for (String classpathEntry : rawClasspathEntries) {
+ File file = new File(classpathEntry);
+ include(null, file, locations);
+ }
+ long end = System.currentTimeMillis();
+ if (debug) {
+ debug("getClassPathLocations took " + (end - start) + "ms");
+ }
+ return locations;
+ }
+
+ /**
+ * Checks a class path entry to see whether it can contain widgets and
+ * widgetsets.
+ *
+ * All directories are automatically accepted. JARs are accepted if they
+ * have the "Vaadin-Widgetsets" attribute in their manifest or the JAR file
+ * name contains "vaadin-" or ".vaadin.".
+ *
+ * Also other non-JAR entries may be accepted, the caller should be prepared
+ * to handle them.
+ *
+ * @param classpathEntry
+ * class path entry string as given in the Java class path
+ * @return true if the entry should be considered when looking for widgets
+ * or widgetsets
+ */
+ private static boolean acceptClassPathEntry(String classpathEntry) {
+ if (!classpathEntry.endsWith(".jar")) {
+ // accept all non jars (practically directories)
+ return true;
+ } else {
+ // accepts jars that comply with vaadin-component packaging
+ // convention (.vaadin. or vaadin- as distribution packages),
+ if (classpathEntry.contains("vaadin-")
+ || classpathEntry.contains(".vaadin.")) {
+ return true;
+ } else {
+ URL url;
+ try {
+ url = new URL("file:"
+ + new File(classpathEntry).getCanonicalPath());
+ url = new URL("jar:" + url.toExternalForm() + "!/");
+ JarURLConnection conn = (JarURLConnection) url
+ .openConnection();
+ debug(url.toString());
+
+ JarFile jarFile = conn.getJarFile();
+ Manifest manifest = jarFile.getManifest();
+ if (manifest != null) {
+ Attributes mainAttributes = manifest
+ .getMainAttributes();
+ if (mainAttributes.getValue("Vaadin-Widgetsets") != null) {
+ return true;
+ }
+ if (mainAttributes.getValue("Vaadin-Stylesheets") != null) {
+ return true;
+ }
+ }
+ } catch (MalformedURLException e) {
+ if (debug) {
+ error("Failed to inspect JAR file", e);
+ }
+ } catch (IOException e) {
+ if (debug) {
+ error("Failed to inspect JAR file", e);
+ }
+ }
+
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Recursively add subdirectories and jar files to locations - see
+ * {@link #classpathLocations}.
+ *
+ * @param name
+ * @param file
+ * @param locations
+ */
+ private final static void include(String name, File file,
+ Map<String, URL> locations) {
+ if (!file.exists()) {
+ return;
+ }
+ if (!file.isDirectory()) {
+ // could be a JAR file
+ includeJar(file, locations);
+ return;
+ }
+
+ if (file.isHidden() || file.getPath().contains(File.separator + ".")) {
+ return;
+ }
+
+ if (name == null) {
+ name = "";
+ } else {
+ name += ".";
+ }
+
+ // add all directories recursively
+ File[] dirs = file.listFiles(DIRECTORIES_ONLY);
+ for (int i = 0; i < dirs.length; i++) {
+ try {
+ // add the present directory
+ if (!dirs[i].isHidden()
+ && !dirs[i].getPath().contains(File.separator + ".")) {
+ String key = dirs[i].getCanonicalPath() + "/" + name
+ + dirs[i].getName();
+ locations.put(key,
+ new URL("file://" + dirs[i].getCanonicalPath()));
+ }
+ } catch (Exception ioe) {
+ return;
+ }
+ include(name + dirs[i].getName(), dirs[i], locations);
+ }
+ }
+
+ /**
+ * Add a jar file to locations - see {@link #classpathLocations}.
+ *
+ * @param name
+ * @param locations
+ */
+ private static void includeJar(File file, Map<String, URL> locations) {
+ try {
+ URL url = new URL("file:" + file.getCanonicalPath());
+ url = new URL("jar:" + url.toExternalForm() + "!/");
+ JarURLConnection conn = (JarURLConnection) url.openConnection();
+ JarFile jarFile = conn.getJarFile();
+ if (jarFile != null) {
+ // the key does not matter here as long as it is unique
+ locations.put(url.toString(), url);
+ }
+ } catch (Exception e) {
+ // e.printStackTrace();
+ return;
+ }
+
+ }
+
+ /**
+ * Find and return the default source directory where to create new
+ * widgetsets.
+ *
+ * Return the first directory (not a JAR file etc.) on the classpath by
+ * default.
+ *
+ * TODO this could be done better...
+ *
+ * @return URL
+ */
+ public static URL getDefaultSourceDirectory() {
++ return getWidgetsetSourceDirectory(null);
++ }
+
++ /**
++ * Find and return the source directory which contains the given widgetset
++ * file.
++ *
++ * If not applicable or widgetsetFileName is null, return the first
++ * directory (not a JAR file etc.) on the classpath.
++ *
++ * TODO this could be done better...
++ *
++ * @param widgetsetFileName
++ * relative path for the widgetset
++ *
++ * @return URL
++ */
++ public static URL getWidgetsetSourceDirectory(String widgetsetFileName) {
+ if (debug) {
+ debug("classpathLocations values:");
+ ArrayList<String> locations = new ArrayList<String>(
+ classpathLocations.keySet());
+ for (String location : locations) {
+ debug(String.valueOf(classpathLocations.get(location)));
+ }
+ }
+
++ URL firstDirectory = null;
+ Iterator<String> it = rawClasspathEntries.iterator();
+ while (it.hasNext()) {
+ String entry = it.next();
+
+ File directory = new File(entry);
+ if (directory.exists() && !directory.isHidden()
+ && directory.isDirectory()) {
+ try {
- } catch (IOException e) {
++ URL directoryUrl = directory.toURI().toURL();
++
++ // Store the first directory encountered.
++ if (firstDirectory == null) {
++ firstDirectory = directoryUrl;
+ }
- return null;
++
++ if (widgetsetFileName == null
++ || new File(directory, widgetsetFileName).exists()) {
++ return directoryUrl;
++ }
++ } catch (MalformedURLException e) {
+ // ignore: continue to the next classpath entry
+ if (debug) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
++
++ return firstDirectory;
+ }
+
+ /**
+ * Test method for helper tool
+ */
+ public static void main(String[] args) {
+ log("Searching for available widgetsets and stylesheets...");
+
+ ClassPathExplorer.getAvailableWidgetSetsAndStylesheets();
+ }
+
+ private static void log(String message) {
+ System.out.println(message);
+ }
+
+ private static void error(String message, Exception e) {
+ System.err.println(message);
+ e.printStackTrace();
+ }
+
+ private static void debug(String message) {
+ if (debug) {
+ System.out.println(message);
+ }
+ }
+
+}
--- /dev/null
- sourceUrl = ClassPathExplorer.getDefaultSourceDirectory();
+/*
+ * Copyright 2000-2014 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.server.widgetsetutils;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to update widgetsets GWT module configuration file. Can be used
+ * command line or via IDE tools.
+ *
+ * <p>
+ * If module definition file contains text "WS Compiler: manually edited", tool
+ * will skip editing file.
+ *
+ */
+public class WidgetSetBuilder {
+
+ public static void main(String[] args) throws IOException {
+ if (args.length == 0) {
+ printUsage();
+ } else {
+ String widgetsetname = args[0];
+ updateWidgetSet(widgetsetname);
+
+ }
+ }
+
+ public static void updateWidgetSet(final String widgetset)
+ throws IOException, FileNotFoundException {
+ boolean changed = false;
+
+ Map<String, URL> availableWidgetSets = ClassPathExplorer
+ .getAvailableWidgetSets();
+
++ String widgetsetFileName = widgetset.replace(".", "/") + ".gwt.xml";
+ URL sourceUrl = availableWidgetSets.get(widgetset);
+ if (sourceUrl == null) {
+ // find first/default source directory
- String widgetsetfilename = sourceUrl.getFile() + "/"
- + widgetset.replace(".", "/") + ".gwt.xml";
++ sourceUrl = ClassPathExplorer
++ .getWidgetsetSourceDirectory(widgetsetFileName);
+ }
+
- File widgetsetFile = new File(widgetsetfilename);
++ String wsFullPath = sourceUrl.getFile() + "/" + widgetsetFileName;
+
- commitChanges(widgetsetfilename, content);
++ File widgetsetFile = new File(wsFullPath);
+ if (!widgetsetFile.exists()) {
+ // create empty gwt module file
+ File parent = widgetsetFile.getParentFile();
+ if (parent != null && !parent.exists()) {
+ if (!parent.mkdirs()) {
+ throw new IOException(
+ "Could not create directory for the widgetset: "
+ + parent.getPath());
+ }
+ }
+ widgetsetFile.createNewFile();
+ PrintStream printStream = new PrintStream(new FileOutputStream(
+ widgetsetFile));
+ printStream
+ .print("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<!DOCTYPE module PUBLIC \"-//Google Inc.//DTD Google Web Toolkit 2.5.1//EN\" \"http://google-web-toolkit.googlecode.com/svn/tags/2.5.1/distro-source/core/src/gwt-module.dtd\">\n");
+ printStream.print("<module>\n");
+ printStream
+ .print(" <!--\n"
+ + " Uncomment the following to compile the widgetset for one browser only.\n\n"
+ + " Multiple browsers can be specified as a comma separated list. The\n"
+ + " supported user agents at the moment of writing were:\n"
+ + " ie8,ie9,gecko1_8,safari,opera\n\n"
+ + " The value gecko1_8 is used for Firefox and safari is used for webkit\n"
+ + " based browsers including Google Chrome.\n"
+ + " -->\n"
+ + " <!-- <set-property name=\"user.agent\" value=\"safari\"/> -->\n\n"
+ + " <!--\n"
+ + " To enable SuperDevMode, uncomment this line.\n\n"
+ + " See https://vaadin.com/wiki/-/wiki/Main/Using%20SuperDevMode for more\n"
+ + " information and instructions.\n"
+ + " -->\n"
+ + " <!-- <set-configuration-property name=\"devModeRedirectEnabled\" value=\"true\" /> -->\n\n");
+ printStream.print("\n</module>\n");
+ printStream.close();
+ changed = true;
+ }
+
+ String content = readFile(widgetsetFile);
+ if (isEditable(content)) {
+ String originalContent = content;
+
+ Collection<String> oldInheritedWidgetsets = getCurrentInheritedWidgetsets(content);
+
+ // add widgetsets that do not exist
+ Iterator<String> i = availableWidgetSets.keySet().iterator();
+ while (i.hasNext()) {
+ String ws = i.next();
+ if (ws.equals(widgetset)) {
+ // do not inherit the module itself
+ continue;
+ }
+ if (!oldInheritedWidgetsets.contains(ws)) {
+ content = addWidgetSet(ws, content);
+ }
+ }
+
+ for (String ws : oldInheritedWidgetsets) {
+ if (!availableWidgetSets.containsKey(ws)) {
+ // widgetset not available in classpath
+ content = removeWidgetSet(ws, content);
+ }
+ }
+
+ changed = changed || !content.equals(originalContent);
+ if (changed) {
++ commitChanges(wsFullPath, content);
+ }
+ } else {
+ System.out
+ .println("Widgetset is manually edited. Skipping updates.");
+ }
+ }
+
+ private static boolean isEditable(String content) {
+ return !content.contains("WS Compiler: manually edited");
+ }
+
+ private static String removeWidgetSet(String ws, String content) {
+ return content.replaceFirst("<inherits name=\"" + ws + "\"[^/]*/>", "");
+ }
+
+ private static void commitChanges(String widgetsetfilename, String content)
+ throws IOException {
+ BufferedWriter bufferedWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(widgetsetfilename)));
+ bufferedWriter.write(content);
+ bufferedWriter.close();
+ }
+
+ private static String addWidgetSet(String ws, String content) {
+ return content.replace("</module>", "\n <inherits name=\"" + ws
+ + "\" />" + "\n</module>");
+ }
+
+ private static Collection<String> getCurrentInheritedWidgetsets(
+ String content) {
+ HashSet<String> hashSet = new HashSet<String>();
+ Pattern inheritsPattern = Pattern.compile(" name=\"([^\"]*)\"");
+
+ Matcher matcher = inheritsPattern.matcher(content);
+
+ while (matcher.find()) {
+ String gwtModule = matcher.group(1);
+ if (isWidgetset(gwtModule)) {
+ hashSet.add(gwtModule);
+ }
+ }
+ return hashSet;
+ }
+
+ static boolean isWidgetset(String gwtModuleName) {
+ return gwtModuleName.toLowerCase().contains("widgetset");
+ }
+
+ private static String readFile(File widgetsetFile) throws IOException {
+ Reader fi = new FileReader(widgetsetFile);
+ BufferedReader bufferedReader = new BufferedReader(fi);
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ sb.append(line);
+ sb.append("\n");
+ }
+ fi.close();
+ return sb.toString();
+ }
+
+ private static void printUsage() {
+ PrintStream o = System.out;
+ o.println(WidgetSetBuilder.class.getSimpleName() + " usage:");
+ o.println(" 1. Set the same classpath as you will "
+ + "have for the GWT compiler.");
+ o.println(" 2. Give the widgetsetname (to be created or updated)"
+ + " as first parameter");
+ o.println();
+ o.println("All found vaadin widgetsets will be inherited in given widgetset");
+
+ }
+
+}
--- /dev/null
- import com.vaadin.shared.ui.AlignmentInfo;
+/*
+ * Copyright 2000-2014 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.ui;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
+import com.vaadin.event.LayoutEvents.LayoutClickEvent;
+import com.vaadin.event.LayoutEvents.LayoutClickListener;
+import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
+import com.vaadin.server.Sizeable;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.EventId;
+import com.vaadin.shared.MouseEventDetails;
- int bitMask = 0;
- if (attr.hasKey(":middle")) {
- bitMask += AlignmentInfo.Bits.ALIGNMENT_VERTICAL_CENTER;
- } else if (attr.hasKey(":bottom")) {
- bitMask += AlignmentInfo.Bits.ALIGNMENT_BOTTOM;
- } else {
- bitMask += AlignmentInfo.Bits.ALIGNMENT_TOP;
- }
- if (attr.hasKey(":center")) {
- bitMask += AlignmentInfo.Bits.ALIGNMENT_HORIZONTAL_CENTER;
- } else if (attr.hasKey(":right")) {
- bitMask += AlignmentInfo.Bits.ALIGNMENT_RIGHT;
- } else {
- bitMask += AlignmentInfo.Bits.ALIGNMENT_LEFT;
- }
- setComponentAlignment(newChild, new Alignment(bitMask));
+import com.vaadin.shared.ui.MarginInfo;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutServerRpc;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState.ChildComponentData;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+
+@SuppressWarnings("serial")
+public abstract class AbstractOrderedLayout extends AbstractLayout implements
+ Layout.AlignmentHandler, Layout.SpacingHandler, LayoutClickNotifier,
+ Layout.MarginHandler {
+
+ private AbstractOrderedLayoutServerRpc rpc = new AbstractOrderedLayoutServerRpc() {
+
+ @Override
+ public void layoutClick(MouseEventDetails mouseDetails,
+ Connector clickedConnector) {
+ fireEvent(LayoutClickEvent.createEvent(AbstractOrderedLayout.this,
+ mouseDetails, clickedConnector));
+ }
+ };
+
+ public static final Alignment ALIGNMENT_DEFAULT = Alignment.TOP_LEFT;
+
+ /**
+ * Custom layout slots containing the components.
+ */
+ protected LinkedList<Component> components = new LinkedList<Component>();
+
+ private Alignment defaultComponentAlignment = Alignment.TOP_LEFT;
+
+ /* Child component alignments */
+
+ /**
+ * Constructs an empty AbstractOrderedLayout.
+ */
+ public AbstractOrderedLayout() {
+ registerRpc(rpc);
+ }
+
+ @Override
+ protected AbstractOrderedLayoutState getState() {
+ return (AbstractOrderedLayoutState) super.getState();
+ }
+
+ @Override
+ protected AbstractOrderedLayoutState getState(boolean markAsDirty) {
+ return (AbstractOrderedLayoutState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Add a component into this container. The component is added to the right
+ * or under the previous component.
+ *
+ * @param c
+ * the component to be added.
+ */
+ @Override
+ public void addComponent(Component c) {
+ // Add to components before calling super.addComponent
+ // so that it is available to AttachListeners
+ components.add(c);
+ try {
+ super.addComponent(c);
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+ componentAdded(c);
+ }
+
+ /**
+ * Adds a component into this container. The component is added to the left
+ * or on top of the other components.
+ *
+ * @param c
+ * the component to be added.
+ */
+ public void addComponentAsFirst(Component c) {
+ // If c is already in this, we must remove it before proceeding
+ // see ticket #7668
+ if (equals(c.getParent())) {
+ removeComponent(c);
+ }
+ components.addFirst(c);
+ try {
+ super.addComponent(c);
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+ componentAdded(c);
+
+ }
+
+ /**
+ * Adds a component into indexed position in this container.
+ *
+ * @param c
+ * the component to be added.
+ * @param index
+ * the index of the component position. The components currently
+ * in and after the position are shifted forwards.
+ */
+ public void addComponent(Component c, int index) {
+ // If c is already in this, we must remove it before proceeding
+ // see ticket #7668
+ if (equals(c.getParent())) {
+ // When c is removed, all components after it are shifted down
+ if (index > getComponentIndex(c)) {
+ index--;
+ }
+ removeComponent(c);
+ }
+ components.add(index, c);
+ try {
+ super.addComponent(c);
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+
+ componentAdded(c);
+ }
+
+ private void componentRemoved(Component c) {
+ getState().childData.remove(c);
+ }
+
+ private void componentAdded(Component c) {
+ ChildComponentData ccd = new ChildComponentData();
+ ccd.alignmentBitmask = getDefaultComponentAlignment().getBitMask();
+ getState().childData.put(c, ccd);
+ }
+
+ /**
+ * Removes the component from this container.
+ *
+ * @param c
+ * the component to be removed.
+ */
+ @Override
+ public void removeComponent(Component c) {
+ components.remove(c);
+ super.removeComponent(c);
+ componentRemoved(c);
+ }
+
+ /**
+ * Gets the component container iterator for going trough all the components
+ * in the container.
+ *
+ * @return the Iterator of the components inside the container.
+ */
+ @Override
+ public Iterator<Component> iterator() {
+ return components.iterator();
+ }
+
+ /**
+ * Gets the number of contained components. Consistent with the iterator
+ * returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components
+ */
+ @Override
+ public int getComponentCount() {
+ return components.size();
+ }
+
+ /* Documented in superclass */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+
+ // Gets the locations
+ int oldLocation = -1;
+ int newLocation = -1;
+ int location = 0;
+ for (final Iterator<Component> i = components.iterator(); i.hasNext();) {
+ final Component component = i.next();
+
+ if (component == oldComponent) {
+ oldLocation = location;
+ }
+ if (component == newComponent) {
+ newLocation = location;
+ }
+
+ location++;
+ }
+
+ if (oldLocation == -1) {
+ addComponent(newComponent);
+ } else if (newLocation == -1) {
+ Alignment alignment = getComponentAlignment(oldComponent);
+ float expandRatio = getExpandRatio(oldComponent);
+
+ removeComponent(oldComponent);
+ addComponent(newComponent, oldLocation);
+ applyLayoutSettings(newComponent, alignment, expandRatio);
+ } else {
+ // Both old and new are in the layout
+ if (oldLocation > newLocation) {
+ components.remove(oldComponent);
+ components.add(newLocation, oldComponent);
+ components.remove(newComponent);
+ components.add(oldLocation, newComponent);
+ } else {
+ components.remove(newComponent);
+ components.add(oldLocation, newComponent);
+ components.remove(oldComponent);
+ components.add(newLocation, oldComponent);
+ }
+
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void setComponentAlignment(Component childComponent,
+ Alignment alignment) {
+ ChildComponentData childData = getState().childData.get(childComponent);
+ if (childData != null) {
+ // Alignments are bit masks
+ childData.alignmentBitmask = alignment.getBitMask();
+ } else {
+ throw new IllegalArgumentException(
+ "Component must be added to layout before using setComponentAlignment()");
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com
+ * .vaadin.ui.Component)
+ */
+ @Override
+ public Alignment getComponentAlignment(Component childComponent) {
+ ChildComponentData childData = getState().childData.get(childComponent);
+ if (childData == null) {
+ throw new IllegalArgumentException(
+ "The given component is not a child of this layout");
+ }
+
+ return new Alignment(childData.alignmentBitmask);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean)
+ */
+ @Override
+ public void setSpacing(boolean spacing) {
+ getState().spacing = spacing;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing()
+ */
+ @Override
+ public boolean isSpacing() {
+ return getState(false).spacing;
+ }
+
+ /**
+ * <p>
+ * This method is used to control how excess space in layout is distributed
+ * among components. Excess space may exist if layout is sized and contained
+ * non relatively sized components don't consume all available space.
+ *
+ * <p>
+ * Example how to distribute 1:3 (33%) for component1 and 2:3 (67%) for
+ * component2 :
+ *
+ * <code>
+ * layout.setExpandRatio(component1, 1);<br>
+ * layout.setExpandRatio(component2, 2);
+ * </code>
+ *
+ * <p>
+ * If no ratios have been set, the excess space is distributed evenly among
+ * all components.
+ *
+ * <p>
+ * Note, that width or height (depending on orientation) needs to be defined
+ * for this method to have any effect.
+ *
+ * @see Sizeable
+ *
+ * @param component
+ * the component in this layout which expand ratio is to be set
+ * @param ratio
+ */
+ public void setExpandRatio(Component component, float ratio) {
+ ChildComponentData childData = getState().childData.get(component);
+ if (childData == null) {
+ throw new IllegalArgumentException(
+ "The given component is not a child of this layout");
+ }
+
+ childData.expandRatio = ratio;
+ }
+
+ /**
+ * Returns the expand ratio of given component.
+ *
+ * @param component
+ * which expand ratios is requested
+ * @return expand ratio of given component, 0.0f by default.
+ */
+ public float getExpandRatio(Component component) {
+ ChildComponentData childData = getState(false).childData.get(component);
+ if (childData == null) {
+ throw new IllegalArgumentException(
+ "The given component is not a child of this layout");
+ }
+
+ return childData.expandRatio;
+ }
+
+ @Override
+ public void addLayoutClickListener(LayoutClickListener listener) {
+ addListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener,
+ LayoutClickListener.clickMethod);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addLayoutClickListener(LayoutClickListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(LayoutClickListener listener) {
+ addLayoutClickListener(listener);
+ }
+
+ @Override
+ public void removeLayoutClickListener(LayoutClickListener listener) {
+ removeListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeLayoutClickListener(LayoutClickListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(LayoutClickListener listener) {
+ removeLayoutClickListener(listener);
+ }
+
+ /**
+ * Returns the index of the given component.
+ *
+ * @param component
+ * The component to look up.
+ * @return The index of the component or -1 if the component is not a child.
+ */
+ public int getComponentIndex(Component component) {
+ return components.indexOf(component);
+ }
+
+ /**
+ * Returns the component at the given position.
+ *
+ * @param index
+ * The position of the component.
+ * @return The component at the given index.
+ * @throws IndexOutOfBoundsException
+ * If the index is out of range.
+ */
+ public Component getComponent(int index) throws IndexOutOfBoundsException {
+ return components.get(index);
+ }
+
+ @Override
+ public void setMargin(boolean enabled) {
+ setMargin(new MarginInfo(enabled));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.MarginHandler#getMargin()
+ */
+ @Override
+ public MarginInfo getMargin() {
+ return new MarginInfo(getState(false).marginsBitmask);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.MarginHandler#setMargin(MarginInfo)
+ */
+ @Override
+ public void setMargin(MarginInfo marginInfo) {
+ getState().marginsBitmask = marginInfo.getBitMask();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.AlignmentHandler#getDefaultComponentAlignment()
+ */
+ @Override
+ public Alignment getDefaultComponentAlignment() {
+ return defaultComponentAlignment;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.ui.Layout.AlignmentHandler#setDefaultComponentAlignment(com
+ * .vaadin.ui.Alignment)
+ */
+ @Override
+ public void setDefaultComponentAlignment(Alignment defaultAlignment) {
+ defaultComponentAlignment = defaultAlignment;
+ }
+
+ private void applyLayoutSettings(Component target, Alignment alignment,
+ float expandRatio) {
+ setComponentAlignment(target, alignment);
+ setExpandRatio(target, expandRatio);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ // process default attributes
+ super.readDesign(design, designContext);
+
+ setMargin(readMargin(design, getMargin(), designContext));
+
+ // handle children
+ for (Element childComponent : design.children()) {
+ Attributes attr = childComponent.attributes();
+ Component newChild = designContext.readDesign(childComponent);
+ addComponent(newChild);
+ // handle alignment
++ setComponentAlignment(newChild,
++ DesignAttributeHandler.readAlignment(attr));
+ // handle expand ratio
+ if (attr.hasKey(":expand")) {
+ String value = attr.get(":expand");
+ if (value.length() > 0) {
+ try {
+ float ratio = Float.valueOf(value);
+ setExpandRatio(newChild, ratio);
+ } catch (NumberFormatException nfe) {
+ getLogger().info(
+ "Failed to parse expand ratio " + value);
+ }
+ } else {
+ setExpandRatio(newChild, 1.0f);
+ }
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ // write default attributes
+ super.writeDesign(design, designContext);
+
+ AbstractOrderedLayout def = (AbstractOrderedLayout) designContext
+ .getDefaultInstance(this);
+
+ writeMargin(design, getMargin(), def.getMargin(), designContext);
+
+ // handle children
+ if (!designContext.shouldWriteChildren(this, def)) {
+ return;
+ }
+
+ for (Component child : this) {
+ Element childElement = designContext.createElement(child);
+ design.appendChild(childElement);
+ // handle alignment
+ Alignment alignment = getComponentAlignment(child);
+ if (alignment.isMiddle()) {
+ childElement.attr(":middle", true);
+ } else if (alignment.isBottom()) {
+ childElement.attr(":bottom", true);
+ }
+ if (alignment.isCenter()) {
+ childElement.attr(":center", true);
+ } else if (alignment.isRight()) {
+ childElement.attr(":right", true);
+ }
+ // handle expand ratio
+ float expandRatio = getExpandRatio(child);
+ if (expandRatio == 1.0f) {
+ childElement.attr(":expand", true);
+ } else if (expandRatio > 0) {
+ childElement.attr(":expand", DesignAttributeHandler
+ .getFormatter().format(expandRatio));
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#getCustomAttributes()
+ */
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> customAttributes = super.getCustomAttributes();
+ customAttributes.add("margin");
+ customAttributes.add("margin-left");
+ customAttributes.add("margin-right");
+ customAttributes.add("margin-top");
+ customAttributes.add("margin-bottom");
+ return customAttributes;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(AbstractOrderedLayout.class.getName());
+ }
+}
--- /dev/null
- if (!isNullSelectionAllowed() && id == null) {
- markAsDirty();
- } else if (id != null
- && id.equals(getNullSelectionItemId())) {
- setValue(null, true);
+/*
+ * Copyright 2000-2014 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.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jsoup.nodes.Element;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Validator.InvalidValueException;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.Converter.ConversionException;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.event.DataBoundTransferable;
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetailsImpl;
+import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion;
+import com.vaadin.event.dd.acceptcriteria.ContainsDataFlavor;
+import com.vaadin.event.dd.acceptcriteria.TargetDetailIs;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.Resource;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.ui.combobox.FilteringMode;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.shared.ui.select.AbstractSelectState;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.ui.declarative.DesignFormatter;
+
+/**
+ * <p>
+ * A class representing a selection of items the user has selected in a UI. The
+ * set of choices is presented as a set of {@link com.vaadin.data.Item}s in a
+ * {@link com.vaadin.data.Container}.
+ * </p>
+ *
+ * <p>
+ * A <code>Select</code> component may be in single- or multiselect mode.
+ * Multiselect mode means that more than one item can be selected
+ * simultaneously.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+// TODO currently cannot specify type more precisely in case of multi-select
+public abstract class AbstractSelect extends AbstractField<Object> implements
+ Container, Container.Viewer, Container.PropertySetChangeListener,
+ Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier,
+ Container.ItemSetChangeListener, LegacyComponent {
+
+ public enum ItemCaptionMode {
+ /**
+ * Item caption mode: Item's ID converted to a String using
+ * {@link VaadinSession#getConverterFactory()} is used as caption.
+ */
+ ID,
+ /**
+ * Item caption mode: Item's ID's <code>String</code> representation is
+ * used as caption.
+ *
+ * @since 7.5.6
+ */
+ ID_TOSTRING,
+ /**
+ * Item caption mode: Item's <code>String</code> representation is used
+ * as caption.
+ */
+ ITEM,
+ /**
+ * Item caption mode: Index of the item is used as caption. The index
+ * mode can only be used with the containers implementing the
+ * {@link com.vaadin.data.Container.Indexed} interface.
+ */
+ INDEX,
+ /**
+ * Item caption mode: If an Item has a caption it's used, if not, Item's
+ * ID converted to a String using
+ * {@link VaadinSession#getConverterFactory()} is used as caption.
+ * <b>This is the default</b>.
+ */
+ EXPLICIT_DEFAULTS_ID,
+ /**
+ * Item caption mode: Captions must be explicitly specified.
+ */
+ EXPLICIT,
+ /**
+ * Item caption mode: Only icons are shown, captions are hidden.
+ */
+ ICON_ONLY,
+ /**
+ * Item caption mode: Item captions are read from property specified
+ * with <code>setItemCaptionPropertyId</code>.
+ */
+ PROPERTY;
+ }
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ID = ItemCaptionMode.ID;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#ITEM} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ITEM = ItemCaptionMode.ITEM;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#INDEX} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_INDEX = ItemCaptionMode.INDEX;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#EXPLICIT_DEFAULTS_ID}
+ * instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID = ItemCaptionMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#EXPLICIT} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT = ItemCaptionMode.EXPLICIT;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#ICON_ONLY} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ICON_ONLY = ItemCaptionMode.ICON_ONLY;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#PROPERTY} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_PROPERTY = ItemCaptionMode.PROPERTY;
+
+ /**
+ * Interface for option filtering, used to filter options based on user
+ * entered value. The value is matched to the item caption.
+ * <code>FilteringMode.OFF</code> (0) turns the filtering off.
+ * <code>FilteringMode.STARTSWITH</code> (1) matches from the start of the
+ * caption. <code>FilteringMode.CONTAINS</code> (1) matches anywhere in the
+ * caption.
+ */
+ public interface Filtering extends Serializable {
+
+ /**
+ * @deprecated As of 7.0, use {@link FilteringMode#OFF} instead
+ */
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_OFF = FilteringMode.OFF;
+ /**
+ * @deprecated As of 7.0, use {@link FilteringMode#STARTSWITH} instead
+ */
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_STARTSWITH = FilteringMode.STARTSWITH;
+ /**
+ * @deprecated As of 7.0, use {@link FilteringMode#CONTAINS} instead
+ */
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_CONTAINS = FilteringMode.CONTAINS;
+
+ /**
+ * Sets the option filtering mode.
+ *
+ * @param filteringMode
+ * the filtering mode to use
+ */
+ public void setFilteringMode(FilteringMode filteringMode);
+
+ /**
+ * Gets the current filtering mode.
+ *
+ * @return the filtering mode in use
+ */
+ public FilteringMode getFilteringMode();
+
+ }
+
+ /**
+ * Is the select in multiselect mode?
+ */
+ private boolean multiSelect = false;
+
+ /**
+ * Select options.
+ */
+ protected Container items;
+
+ /**
+ * Is the user allowed to add new options?
+ */
+ private boolean allowNewOptions;
+
+ /**
+ * Keymapper used to map key values.
+ */
+ protected KeyMapper<Object> itemIdMapper = new KeyMapper<Object>();
+
+ /**
+ * Item icons.
+ */
+ private final HashMap<Object, Resource> itemIcons = new HashMap<Object, Resource>();
+
+ /**
+ * Item captions.
+ */
+ private final HashMap<Object, String> itemCaptions = new HashMap<Object, String>();
+
+ /**
+ * Item caption mode.
+ */
+ private ItemCaptionMode itemCaptionMode = ItemCaptionMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * Item caption source property id.
+ */
+ private Object itemCaptionPropertyId = null;
+
+ /**
+ * Item icon source property id.
+ */
+ private Object itemIconPropertyId = null;
+
+ /**
+ * List of property set change event listeners.
+ */
+ private Set<Container.PropertySetChangeListener> propertySetEventListeners = null;
+
+ /**
+ * List of item set change event listeners.
+ */
+ private Set<Container.ItemSetChangeListener> itemSetEventListeners = null;
+
+ /**
+ * Item id that represents null selection of this select.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ */
+ private Object nullSelectionItemId = null;
+
+ // Null (empty) selection is enabled by default
+ private boolean nullSelectionAllowed = true;
+ private NewItemHandler newItemHandler;
+
+ // Caption (Item / Property) change listeners
+ CaptionChangeListener captionChangeListener;
+
+ /* Constructors */
+
+ /**
+ * Creates an empty Select. The caption is not used.
+ */
+ public AbstractSelect() {
+ setContainerDataSource(new IndexedContainer());
+ }
+
+ /**
+ * Creates an empty Select with caption.
+ */
+ public AbstractSelect(String caption) {
+ setContainerDataSource(new IndexedContainer());
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new select that is connected to a data-source.
+ *
+ * @param caption
+ * the Caption of the component.
+ * @param dataSource
+ * the Container datasource to be selected from by this select.
+ */
+ public AbstractSelect(String caption, Container dataSource) {
+ setCaption(caption);
+ setContainerDataSource(dataSource);
+ }
+
+ /**
+ * Creates a new select that is filled from a collection of option values.
+ *
+ * @param caption
+ * the Caption of this field.
+ * @param options
+ * the Collection containing the options.
+ */
+ public AbstractSelect(String caption, Collection<?> options) {
+
+ // Creates the options container and add given options to it
+ final Container c = new IndexedContainer();
+ if (options != null) {
+ for (final Iterator<?> i = options.iterator(); i.hasNext();) {
+ c.addItem(i.next());
+ }
+ }
+
+ setCaption(caption);
+ setContainerDataSource(c);
+ }
+
+ /* Component methods */
+
+ /**
+ * Paints the content of this component.
+ *
+ * @param target
+ * the Paint Event.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ // Paints select attributes
+ if (isMultiSelect()) {
+ target.addAttribute("selectmode", "multi");
+ }
+ if (isNewItemsAllowed()) {
+ target.addAttribute("allownewitem", true);
+ }
+ if (isNullSelectionAllowed()) {
+ target.addAttribute("nullselect", true);
+ if (getNullSelectionItemId() != null) {
+ target.addAttribute("nullselectitem", true);
+ }
+ }
+
+ // Constructs selected keys array
+ String[] selectedKeys;
+ if (isMultiSelect()) {
+ selectedKeys = new String[((Set<?>) getValue()).size()];
+ } else {
+ selectedKeys = new String[(getValue() == null
+ && getNullSelectionItemId() == null ? 0 : 1)];
+ }
+
+ // ==
+ // first remove all previous item/property listeners
+ getCaptionChangeListener().clear();
+ // Paints the options and create array of selected id keys
+
+ target.startTag("options");
+ int keyIndex = 0;
+ // Support for external null selection item id
+ final Collection<?> ids = getItemIds();
+ if (isNullSelectionAllowed() && getNullSelectionItemId() != null
+ && !ids.contains(getNullSelectionItemId())) {
+ final Object id = getNullSelectionItemId();
+ // Paints option
+ target.startTag("so");
+ paintItem(target, id);
+ if (isSelected(id)) {
+ selectedKeys[keyIndex++] = itemIdMapper.key(id);
+ }
+ target.endTag("so");
+ }
+
+ final Iterator<?> i = getItemIds().iterator();
+ // Paints the available selection options from data source
+ while (i.hasNext()) {
+ // Gets the option attribute values
+ final Object id = i.next();
+ if (!isNullSelectionAllowed() && id != null
+ && id.equals(getNullSelectionItemId())) {
+ // Remove item if it's the null selection item but null
+ // selection is not allowed
+ continue;
+ }
+ final String key = itemIdMapper.key(id);
+ // add listener for each item, to cause repaint if an item changes
+ getCaptionChangeListener().addNotifierForItem(id);
+ target.startTag("so");
+ paintItem(target, id);
+ if (isSelected(id) && keyIndex < selectedKeys.length) {
+ selectedKeys[keyIndex++] = key;
+ }
+ target.endTag("so");
+ }
+ target.endTag("options");
+ // ==
+
+ // Paint variables
+ target.addVariable(this, "selected", selectedKeys);
+ if (isNewItemsAllowed()) {
+ target.addVariable(this, "newitem", "");
+ }
+
+ }
+
+ protected void paintItem(PaintTarget target, Object itemId)
+ throws PaintException {
+ final String key = itemIdMapper.key(itemId);
+ final String caption = getItemCaption(itemId);
+ final Resource icon = getItemIcon(itemId);
+ if (icon != null) {
+ target.addAttribute("icon", icon);
+ }
+ target.addAttribute("caption", caption);
+ if (itemId != null && itemId.equals(getNullSelectionItemId())) {
+ target.addAttribute("nullselection", true);
+ }
+ target.addAttribute("key", key);
+ if (isSelected(itemId)) {
+ target.addAttribute("selected", true);
+ }
+ }
+
+ /**
+ * Invoked when the value of a variable has changed.
+ *
+ * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ // New option entered (and it is allowed)
+ if (isNewItemsAllowed()) {
+ final String newitem = (String) variables.get("newitem");
+ if (newitem != null && newitem.length() > 0) {
+ getNewItemHandler().addNewItem(newitem);
+ }
+ }
+
+ // Selection change
+ if (variables.containsKey("selected")) {
+ final String[] clientSideSelectedKeys = (String[]) variables
+ .get("selected");
+
+ // Multiselect mode
+ if (isMultiSelect()) {
+
+ // TODO Optimize by adding repaintNotNeeded when applicable
+
+ // Converts the key-array to id-set
+ final LinkedList<Object> acceptedSelections = new LinkedList<Object>();
+ for (int i = 0; i < clientSideSelectedKeys.length; i++) {
+ final Object id = itemIdMapper
+ .get(clientSideSelectedKeys[i]);
+ if (!isNullSelectionAllowed()
+ && (id == null || id == getNullSelectionItemId())) {
+ // skip empty selection if nullselection is not allowed
+ markAsDirty();
+ } else if (id != null && containsId(id)) {
+ acceptedSelections.add(id);
+ }
+ }
+
+ if (!isNullSelectionAllowed() && acceptedSelections.size() < 1) {
+ // empty selection not allowed, keep old value
+ markAsDirty();
+ return;
+ }
+
+ // Limits the deselection to the set of visible items
+ // (non-visible items can not be deselected)
+ Collection<?> visibleNotSelected = getVisibleItemIds();
+ if (visibleNotSelected != null) {
+ visibleNotSelected = new HashSet<Object>(visibleNotSelected);
+ // Don't remove those that will be added to preserve order
+ visibleNotSelected.removeAll(acceptedSelections);
+
+ @SuppressWarnings("unchecked")
+ Set<Object> newsel = (Set<Object>) getValue();
+ if (newsel == null) {
+ newsel = new LinkedHashSet<Object>();
+ } else {
+ newsel = new LinkedHashSet<Object>(newsel);
+ }
+ newsel.removeAll(visibleNotSelected);
+ newsel.addAll(acceptedSelections);
+ setValue(newsel, true);
+ }
+ } else {
+ // Single select mode
+ if (!isNullSelectionAllowed()
+ && (clientSideSelectedKeys.length == 0
+ || clientSideSelectedKeys[0] == null || clientSideSelectedKeys[0] == getNullSelectionItemId())) {
+ markAsDirty();
+ return;
+ }
+ if (clientSideSelectedKeys.length == 0) {
+ // Allows deselection only if the deselected item is
+ // visible
+ final Object current = getValue();
+ final Collection<?> visible = getVisibleItemIds();
+ if (visible != null && visible.contains(current)) {
+ setValue(null, true);
+ }
+ } else {
+ final Object id = itemIdMapper
+ .get(clientSideSelectedKeys[0]);
- setValue(id, true);
++
++ if (id != null) {
++ if (isNullSelectionAllowed()
++ && id.equals(getNullSelectionItemId())) {
++ setValue(null, true);
++ } else {
++ setValue(id, true);
++ }
+ } else {
++ markAsDirty();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * TODO refine doc Setter for new item handler that is called when user adds
+ * new item in newItemAllowed mode.
+ *
+ * @param newItemHandler
+ */
+ public void setNewItemHandler(NewItemHandler newItemHandler) {
+ this.newItemHandler = newItemHandler;
+ }
+
+ /**
+ * TODO refine doc
+ *
+ * @return
+ */
+ public NewItemHandler getNewItemHandler() {
+ if (newItemHandler == null) {
+ newItemHandler = new DefaultNewItemHandler();
+ }
+ return newItemHandler;
+ }
+
+ public interface NewItemHandler extends Serializable {
+ void addNewItem(String newItemCaption);
+ }
+
+ /**
+ * TODO refine doc
+ *
+ * This is a default class that handles adding new items that are typed by
+ * user to selects container.
+ *
+ * By extending this class one may implement some logic on new item addition
+ * like database inserts.
+ *
+ */
+ public class DefaultNewItemHandler implements NewItemHandler {
+ @Override
+ public void addNewItem(String newItemCaption) {
+ // Checks for readonly
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Adds new option
+ if (addItem(newItemCaption) != null) {
+
+ // Sets the caption property, if used
+ if (getItemCaptionPropertyId() != null) {
+ getContainerProperty(newItemCaption,
+ getItemCaptionPropertyId())
+ .setValue(newItemCaption);
+ }
+ if (isMultiSelect()) {
+ Set values = new HashSet((Collection) getValue());
+ values.add(newItemCaption);
+ setValue(values);
+ } else {
+ setValue(newItemCaption);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the visible item ids. In Select, this returns list of all item ids,
+ * but can be overriden in subclasses if they paint only part of the items
+ * to the terminal or null if no items is visible.
+ */
+ public Collection<?> getVisibleItemIds() {
+ return getItemIds();
+ }
+
+ /* Property methods */
+
+ /**
+ * Returns the type of the property. <code>getValue</code> and
+ * <code>setValue</code> methods must be compatible with this type: one can
+ * safely cast <code>getValue</code> to given type and pass any variable
+ * assignable to this type as a parameter to <code>setValue</code>.
+ *
+ * @return the Type of the property.
+ */
+ @Override
+ public Class<?> getType() {
+ if (isMultiSelect()) {
+ return Set.class;
+ } else {
+ return Object.class;
+ }
+ }
+
+ /**
+ * Gets the selected item id or in multiselect mode a set of selected ids.
+ *
+ * @see com.vaadin.ui.AbstractField#getValue()
+ */
+ @Override
+ public Object getValue() {
+ final Object retValue = super.getValue();
+
+ if (isMultiSelect()) {
+
+ // If the return value is not a set
+ if (retValue == null) {
+ return new HashSet<Object>();
+ }
+ if (retValue instanceof Set) {
+ return Collections.unmodifiableSet((Set<?>) retValue);
+ } else if (retValue instanceof Collection) {
+ return new HashSet<Object>((Collection<?>) retValue);
+ } else {
+ final Set<Object> s = new HashSet<Object>();
+ if (items.containsId(retValue)) {
+ s.add(retValue);
+ }
+ return s;
+ }
+
+ } else {
+ return retValue;
+ }
+ }
+
+ /**
+ * Sets the visible value of the property.
+ *
+ * <p>
+ * The value of the select is the selected item id. If the select is in
+ * multiselect-mode, the value is a set of selected item keys. In
+ * multiselect mode all collections of id:s can be assigned.
+ * </p>
+ *
+ * @param newValue
+ * the New selected item or collection of selected items.
+ * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+ if (newValue == getNullSelectionItemId()) {
+ newValue = null;
+ }
+
+ setValue(newValue, false);
+ }
+
+ /**
+ * Sets the visible value of the property.
+ *
+ * <p>
+ * The value of the select is the selected item id. If the select is in
+ * multiselect-mode, the value is a set of selected item keys. In
+ * multiselect mode all collections of id:s can be assigned.
+ * </p>
+ *
+ * @since 7.5.7
+ * @param newValue
+ * the New selected item or collection of selected items.
+ * @param repaintIsNotNeeded
+ * True if caller is sure that repaint is not needed.
+ * @param ignoreReadOnly
+ * True if read-only check should be omitted.
+ * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object,
+ * java.lang.Boolean)
+ */
+ @Override
+ protected void setValue(Object newFieldValue, boolean repaintIsNotNeeded,
+ boolean ignoreReadOnly)
+ throws com.vaadin.data.Property.ReadOnlyException,
+ ConversionException, InvalidValueException {
+ if (isMultiSelect()) {
+ if (newFieldValue == null) {
+ super.setValue(new LinkedHashSet<Object>(), repaintIsNotNeeded,
+ ignoreReadOnly);
+ } else if (Collection.class.isAssignableFrom(newFieldValue
+ .getClass())) {
+ super.setValue(new LinkedHashSet<Object>(
+ (Collection<?>) newFieldValue), repaintIsNotNeeded,
+ ignoreReadOnly);
+ }
+ } else if (newFieldValue == null || items.containsId(newFieldValue)) {
+ super.setValue(newFieldValue, repaintIsNotNeeded, ignoreReadOnly);
+ }
+ }
+
+ /* Container methods */
+
+ /**
+ * Gets the item from the container with given id. If the container does not
+ * contain the requested item, null is returned.
+ *
+ * @param itemId
+ * the item id.
+ * @return the item from the container.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+ return items.getItem(itemId);
+ }
+
+ /**
+ * Gets the item Id collection from the container.
+ *
+ * @return the Collection of item ids.
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ return items.getItemIds();
+ }
+
+ /**
+ * Gets the property Id collection from the container.
+ *
+ * @return the Collection of property ids.
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return items.getContainerPropertyIds();
+ }
+
+ /**
+ * Gets the property type.
+ *
+ * @param propertyId
+ * the Id identifying the property.
+ * @see com.vaadin.data.Container#getType(java.lang.Object)
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return items.getType(propertyId);
+ }
+
+ /*
+ * Gets the number of items in the container.
+ *
+ * @return the Number of items in the container.
+ *
+ * @see com.vaadin.data.Container#size()
+ */
+ @Override
+ public int size() {
+ int size = items.size();
+ assert size >= 0;
+ return size;
+ }
+
+ /**
+ * Tests, if the collection contains an item with given id.
+ *
+ * @param itemId
+ * the Id the of item to be tested.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+ if (itemId != null) {
+ return items.containsId(itemId);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container
+ *
+ * @see com.vaadin.data.Container#getContainerProperty(Object, Object)
+ */
+ @Override
+ public Property getContainerProperty(Object itemId, Object propertyId) {
+ return items.getContainerProperty(itemId, propertyId);
+ }
+
+ /**
+ * Adds the new property to all items. Adds a property with given id, type
+ * and default value to all items in the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @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 {
+
+ final boolean retval = items.addContainerProperty(propertyId, type,
+ defaultValue);
+ if (retval && !(items instanceof Container.PropertySetChangeNotifier)) {
+ firePropertySetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Removes all items from the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+
+ final boolean retval = items.removeAllItems();
+ itemIdMapper.removeAll();
+ if (retval) {
+ setValue(null);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ }
+ return retval;
+ }
+
+ /**
+ * Creates a new item into container with container managed id. The id of
+ * the created new item is returned. The item can be fetched with getItem()
+ * method. if the creation fails, null is returned.
+ *
+ * @return the Id of the created item or null in case of failure.
+ * @see com.vaadin.data.Container#addItem()
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+
+ final Object retval = items.addItem();
+ if (retval != null
+ && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Create a new item into container. The created new item is returned and
+ * ready for setting property values. if the creation fails, null is
+ * returned. In case the container already contains the item, null is
+ * returned.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns null.
+ *
+ * @param itemId
+ * the Identification of the item to be created.
+ * @return the Created item with the given id, or null in case of failure.
+ * @see com.vaadin.data.Container#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+
+ final Item retval = items.addItem(itemId);
+ if (retval != null
+ && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Adds given items with given item ids to container.
+ *
+ * @since 7.2
+ * @param itemId
+ * item identifiers to be added to underlying container
+ * @throws UnsupportedOperationException
+ * if the underlying container don't support adding items with
+ * identifiers
+ */
+ public void addItems(Object... itemId) throws UnsupportedOperationException {
+ for (Object id : itemId) {
+ addItem(id);
+ }
+ }
+
+ /**
+ * Adds given items with given item ids to container.
+ *
+ * @since 7.2
+ * @param itemIds
+ * item identifiers to be added to underlying container
+ * @throws UnsupportedOperationException
+ * if the underlying container don't support adding items with
+ * identifiers
+ */
+ public void addItems(Collection<?> itemIds)
+ throws UnsupportedOperationException {
+ addItems(itemIds.toArray());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+
+ unselect(itemId);
+ final boolean retval = items.removeItem(itemId);
+ itemIdMapper.remove(itemId);
+ if (retval && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Checks that the current selection is valid, i.e. the selected item ids
+ * exist in the container. Updates the selection if one or several selected
+ * item ids are no longer available in the container.
+ */
+ @SuppressWarnings("unchecked")
+ public void sanitizeSelection() {
+ Object value = getValue();
+ if (value == null) {
+ return;
+ }
+
+ boolean changed = false;
+
+ if (isMultiSelect()) {
+ Collection<Object> valueAsCollection = (Collection<Object>) value;
+ List<Object> newSelection = new ArrayList<Object>(
+ valueAsCollection.size());
+ for (Object subValue : valueAsCollection) {
+ if (containsId(subValue)) {
+ newSelection.add(subValue);
+ } else {
+ changed = true;
+ }
+ }
+ if (changed) {
+ setValue(newSelection);
+ }
+ } else {
+ if (!containsId(value)) {
+ setValue(null);
+ }
+ }
+
+ }
+
+ /**
+ * Removes the property from all items. Removes a property with given id
+ * from all the items in the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object)
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+
+ final boolean retval = items.removeContainerProperty(propertyId);
+ if (retval && !(items instanceof Container.PropertySetChangeNotifier)) {
+ firePropertySetChange();
+ }
+ return retval;
+ }
+
+ /* Container.Viewer methods */
+
+ /**
+ * Sets the Container that serves as the data source of the viewer.
+ *
+ * As a side-effect the fields value (selection) is set to null due old
+ * selection not necessary exists in new Container.
+ *
+ * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container)
+ *
+ * @param newDataSource
+ * the new data source.
+ */
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ if (newDataSource == null) {
+ newDataSource = new IndexedContainer();
+ }
+
+ getCaptionChangeListener().clear();
+
+ if (items != newDataSource) {
+
+ // Removes listeners from the old datasource
+ if (items != null) {
+ if (items instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) items)
+ .removeItemSetChangeListener(this);
+ }
+ if (items instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) items)
+ .removePropertySetChangeListener(this);
+ }
+ }
+
+ // Assigns new data source
+ items = newDataSource;
+
+ // Clears itemIdMapper also
+ itemIdMapper.removeAll();
+
+ // Adds listeners
+ if (items != null) {
+ if (items instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) items)
+ .addItemSetChangeListener(this);
+ }
+ if (items instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) items)
+ .addPropertySetChangeListener(this);
+ }
+ }
+
+ /*
+ * We expect changing the data source should also clean value. See
+ * #810, #4607, #5281
+ */
+ setValue(null);
+
+ markAsDirty();
+
+ }
+ }
+
+ /**
+ * Gets the viewing data-source container.
+ *
+ * @see com.vaadin.data.Container.Viewer#getContainerDataSource()
+ */
+ @Override
+ public Container getContainerDataSource() {
+ return items;
+ }
+
+ /* Select attributes */
+
+ /**
+ * Is the select in multiselect mode? In multiselect mode
+ *
+ * @return the Value of property multiSelect.
+ */
+ public boolean isMultiSelect() {
+ return multiSelect;
+ }
+
+ /**
+ * Sets the multiselect mode. Setting multiselect mode false may lose
+ * selection information: if selected items set contains one or more
+ * selected items, only one of the selected items is kept as selected.
+ *
+ * Subclasses of AbstractSelect can choose not to support changing the
+ * multiselect mode, and may throw {@link UnsupportedOperationException}.
+ *
+ * @param multiSelect
+ * the New value of property multiSelect.
+ */
+ public void setMultiSelect(boolean multiSelect) {
+ if (multiSelect && getNullSelectionItemId() != null) {
+ throw new IllegalStateException(
+ "Multiselect and NullSelectionItemId can not be set at the same time.");
+ }
+ if (multiSelect != this.multiSelect) {
+
+ // Selection before mode change
+ final Object oldValue = getValue();
+
+ this.multiSelect = multiSelect;
+
+ // Convert the value type
+ if (multiSelect) {
+ final Set<Object> s = new HashSet<Object>();
+ if (oldValue != null) {
+ s.add(oldValue);
+ }
+ setValue(s);
+ } else {
+ final Set<?> s = (Set<?>) oldValue;
+ if (s == null || s.isEmpty()) {
+ setValue(null);
+ } else {
+ // Set the single select to contain only the first
+ // selected value in the multiselect
+ setValue(s.iterator().next());
+ }
+ }
+
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Does the select allow adding new options by the user. If true, the new
+ * options can be added to the Container. The text entered by the user is
+ * used as id. Note that data-source must allow adding new items.
+ *
+ * @return True if additions are allowed.
+ */
+ public boolean isNewItemsAllowed() {
+ return allowNewOptions;
+ }
+
+ /**
+ * Enables or disables possibility to add new options by the user.
+ *
+ * @param allowNewOptions
+ * the New value of property allowNewOptions.
+ */
+ public void setNewItemsAllowed(boolean allowNewOptions) {
+
+ // Only handle change requests
+ if (this.allowNewOptions != allowNewOptions) {
+
+ this.allowNewOptions = allowNewOptions;
+
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Override the caption of an item. Setting caption explicitly overrides id,
+ * item and index captions.
+ *
+ * @param itemId
+ * the id of the item to be recaptioned.
+ * @param caption
+ * the New caption.
+ */
+ public void setItemCaption(Object itemId, String caption) {
+ if (itemId != null) {
+ itemCaptions.put(itemId, caption);
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the caption of an item. The caption is generated as specified by the
+ * item caption mode. See <code>setItemCaptionMode()</code> for more
+ * details.
+ *
+ * @param itemId
+ * the id of the item to be queried.
+ * @return the caption for specified item.
+ */
+ public String getItemCaption(Object itemId) {
+
+ // Null items can not be found
+ if (itemId == null) {
+ return null;
+ }
+
+ String caption = null;
+
+ switch (getItemCaptionMode()) {
+
+ case ID:
+ caption = idToCaption(itemId);
+ break;
+ case ID_TOSTRING:
+ caption = itemId.toString();
+ break;
+ case INDEX:
+ if (items instanceof Container.Indexed) {
+ caption = String.valueOf(((Container.Indexed) items)
+ .indexOfId(itemId));
+ } else {
+ caption = "ERROR: Container is not indexed";
+ }
+ break;
+
+ case ITEM:
+ final Item i = getItem(itemId);
+ if (i != null) {
+ caption = i.toString();
+ }
+ break;
+
+ case EXPLICIT:
+ caption = itemCaptions.get(itemId);
+ break;
+
+ case EXPLICIT_DEFAULTS_ID:
+ caption = itemCaptions.get(itemId);
+ if (caption == null) {
+ caption = idToCaption(itemId);
+ }
+ break;
+
+ case PROPERTY:
+ final Property<?> p = getContainerProperty(itemId,
+ getItemCaptionPropertyId());
+ if (p != null) {
+ Object value = p.getValue();
+ if (value != null) {
+ caption = value.toString();
+ }
+ }
+ break;
+ }
+
+ // All items must have some captions
+ return caption != null ? caption : "";
+ }
+
+ private String idToCaption(Object itemId) {
+ try {
+ Converter<String, Object> c = (Converter<String, Object>) ConverterUtil
+ .getConverter(String.class, itemId.getClass(), getSession());
+ return ConverterUtil.convertFromModel(itemId, String.class, c,
+ getLocale());
+ } catch (Exception e) {
+ return itemId.toString();
+ }
+ }
+
+ /**
+ * Sets the icon for an item.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @param icon
+ * the icon to use or null.
+ */
+ public void setItemIcon(Object itemId, Resource icon) {
+ if (itemId != null) {
+ if (icon == null) {
+ itemIcons.remove(itemId);
+ } else {
+ itemIcons.put(itemId, icon);
+ }
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the item icon.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @return the icon for the item or null, if not specified.
+ */
+ public Resource getItemIcon(Object itemId) {
+ final Resource explicit = itemIcons.get(itemId);
+ if (explicit != null) {
+ return explicit;
+ }
+
+ if (getItemIconPropertyId() == null) {
+ return null;
+ }
+
+ final Property<?> ip = getContainerProperty(itemId,
+ getItemIconPropertyId());
+ if (ip == null) {
+ return null;
+ }
+ final Object icon = ip.getValue();
+ if (icon instanceof Resource) {
+ return (Resource) icon;
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the item caption mode.
+ *
+ * See {@link ItemCaptionMode} for a description of the modes.
+ * <p>
+ * {@link ItemCaptionMode#EXPLICIT_DEFAULTS_ID} is the default mode.
+ * </p>
+ *
+ * @param mode
+ * the One of the modes listed above.
+ */
+ public void setItemCaptionMode(ItemCaptionMode mode) {
+ if (mode != null) {
+ itemCaptionMode = mode;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the item caption mode.
+ *
+ * <p>
+ * The mode can be one of the following ones:
+ * <ul>
+ * <li><code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> : Items
+ * Id-objects <code>toString</code> is used as item caption. If caption is
+ * explicitly specified, it overrides the id-caption.
+ * <li><code>ITEM_CAPTION_MODE_ID</code> : Items Id-objects
+ * <code>toString</code> is used as item caption.</li>
+ * <li><code>ITEM_CAPTION_MODE_ITEM</code> : Item-objects
+ * <code>toString</code> is used as item caption.</li>
+ * <li><code>ITEM_CAPTION_MODE_INDEX</code> : The index of the item is used
+ * as item caption. The index mode can only be used with the containers
+ * implementing <code>Container.Indexed</code> interface.</li>
+ * <li><code>ITEM_CAPTION_MODE_EXPLICIT</code> : The item captions must be
+ * explicitly specified.</li>
+ * <li><code>ITEM_CAPTION_MODE_PROPERTY</code> : The item captions are read
+ * from property, that must be specified with
+ * <code>setItemCaptionPropertyId</code>.</li>
+ * </ul>
+ * The <code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> is the default
+ * mode.
+ * </p>
+ *
+ * @return the One of the modes listed above.
+ */
+ public ItemCaptionMode getItemCaptionMode() {
+ return itemCaptionMode;
+ }
+
+ /**
+ * Sets the item caption property.
+ *
+ * <p>
+ * Setting the id to a existing property implicitly sets the item caption
+ * mode to <code>ITEM_CAPTION_MODE_PROPERTY</code>. If the object is in
+ * <code>ITEM_CAPTION_MODE_PROPERTY</code> mode, setting caption property id
+ * null resets the item caption mode to
+ * <code>ITEM_CAPTION_EXPLICIT_DEFAULTS_ID</code>.
+ * </p>
+ * <p>
+ * Note that the type of the property used for caption must be String
+ * </p>
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @param propertyId
+ * the id of the property.
+ *
+ */
+ public void setItemCaptionPropertyId(Object propertyId) {
+ if (propertyId != null) {
+ itemCaptionPropertyId = propertyId;
+ setItemCaptionMode(ITEM_CAPTION_MODE_PROPERTY);
+ markAsDirty();
+ } else {
+ itemCaptionPropertyId = null;
+ if (getItemCaptionMode() == ITEM_CAPTION_MODE_PROPERTY) {
+ setItemCaptionMode(ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID);
+ }
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the item caption property.
+ *
+ * @return the Id of the property used as item caption source.
+ */
+ public Object getItemCaptionPropertyId() {
+ return itemCaptionPropertyId;
+ }
+
+ /**
+ * Sets the item icon property.
+ *
+ * <p>
+ * If the property id is set to a valid value, each item is given an icon
+ * got from the given property of the items. The type of the property must
+ * be assignable to Resource.
+ * </p>
+ *
+ * <p>
+ * Note : The icons set with <code>setItemIcon</code> function override the
+ * icons from the property.
+ * </p>
+ *
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @param propertyId
+ * the id of the property that specifies icons for items or null
+ * @throws IllegalArgumentException
+ * If the propertyId is not in the container or is not of a
+ * valid type
+ */
+ public void setItemIconPropertyId(Object propertyId)
+ throws IllegalArgumentException {
+ if (propertyId == null) {
+ itemIconPropertyId = null;
+ } else if (!getContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalArgumentException(
+ "Property id not found in the container");
+ } else if (Resource.class.isAssignableFrom(getType(propertyId))) {
+ itemIconPropertyId = propertyId;
+ } else {
+ throw new IllegalArgumentException(
+ "Property type must be assignable to Resource");
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Gets the item icon property.
+ *
+ * <p>
+ * If the property id is set to a valid value, each item is given an icon
+ * got from the given property of the items. The type of the property must
+ * be assignable to Icon.
+ * </p>
+ *
+ * <p>
+ * Note : The icons set with <code>setItemIcon</code> function override the
+ * icons from the property.
+ * </p>
+ *
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @return the Id of the property containing the item icons.
+ */
+ public Object getItemIconPropertyId() {
+ return itemIconPropertyId;
+ }
+
+ /**
+ * Tests if an item is selected.
+ *
+ * <p>
+ * In single select mode testing selection status of the item identified by
+ * {@link #getNullSelectionItemId()} returns true if the value of the
+ * property is null.
+ * </p>
+ *
+ * @param itemId
+ * the Id the of the item to be tested.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public boolean isSelected(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+ if (isMultiSelect()) {
+ return ((Set<?>) getValue()).contains(itemId);
+ } else {
+ final Object value = getValue();
+ return itemId.equals(value == null ? getNullSelectionItemId()
+ : value);
+ }
+ }
+
+ /**
+ * Selects an item.
+ *
+ * <p>
+ * In single select mode selecting item identified by
+ * {@link #getNullSelectionItemId()} sets the value of the property to null.
+ * </p>
+ *
+ * @param itemId
+ * the identifier of Item to be selected.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public void select(Object itemId) {
+ if (!isMultiSelect()) {
+ setValue(itemId);
+ } else if (!isSelected(itemId) && itemId != null
+ && items.containsId(itemId)) {
+ final Set<Object> s = new HashSet<Object>((Set<?>) getValue());
+ s.add(itemId);
+ setValue(s);
+ }
+ }
+
+ /**
+ * Unselects an item.
+ *
+ * @param itemId
+ * the identifier of the Item to be unselected.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public void unselect(Object itemId) {
+ if (isSelected(itemId)) {
+ if (isMultiSelect()) {
+ final Set<Object> s = new HashSet<Object>((Set<?>) getValue());
+ s.remove(itemId);
+ setValue(s);
+ } else {
+ setValue(null);
+ }
+ }
+ }
+
+ /**
+ * Notifies this listener that the Containers contents has changed.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent)
+ */
+ @Override
+ public void containerPropertySetChange(
+ Container.PropertySetChangeEvent event) {
+ firePropertySetChange();
+ }
+
+ /**
+ * Adds a new Property set change listener for this Container.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener)
+ */
+ @Override
+ public void addPropertySetChangeListener(
+ Container.PropertySetChangeListener listener) {
+ if (propertySetEventListeners == null) {
+ propertySetEventListeners = new LinkedHashSet<Container.PropertySetChangeListener>();
+ }
+ propertySetEventListeners.add(listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addPropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(Container.PropertySetChangeListener listener) {
+ addPropertySetChangeListener(listener);
+ }
+
+ /**
+ * Removes a previously registered Property set change listener.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeNotifier#removeListener(com.vaadin.data.Container.PropertySetChangeListener)
+ */
+ @Override
+ public void removePropertySetChangeListener(
+ Container.PropertySetChangeListener listener) {
+ if (propertySetEventListeners != null) {
+ propertySetEventListeners.remove(listener);
+ if (propertySetEventListeners.isEmpty()) {
+ propertySetEventListeners = null;
+ }
+ }
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removePropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ removePropertySetChangeListener(listener);
+ }
+
+ /**
+ * Adds an Item set change listener for the object.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener)
+ */
+ @Override
+ public void addItemSetChangeListener(
+ Container.ItemSetChangeListener listener) {
+ if (itemSetEventListeners == null) {
+ itemSetEventListeners = new LinkedHashSet<Container.ItemSetChangeListener>();
+ }
+ itemSetEventListeners.add(listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(Container.ItemSetChangeListener listener) {
+ addItemSetChangeListener(listener);
+ }
+
+ /**
+ * Removes the Item set change listener from the object.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener)
+ */
+ @Override
+ public void removeItemSetChangeListener(
+ Container.ItemSetChangeListener listener) {
+ if (itemSetEventListeners != null) {
+ itemSetEventListeners.remove(listener);
+ if (itemSetEventListeners.isEmpty()) {
+ itemSetEventListeners = null;
+ }
+ }
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ removeItemSetChangeListener(listener);
+ }
+
+ @Override
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Container.ItemSetChangeEvent.class.isAssignableFrom(eventType)) {
+ if (itemSetEventListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(itemSetEventListeners);
+ }
+ } else if (Container.PropertySetChangeEvent.class
+ .isAssignableFrom(eventType)) {
+ if (propertySetEventListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertySetEventListeners);
+ }
+ }
+
+ return super.getListeners(eventType);
+ }
+
+ /**
+ * Lets the listener know a Containers Item set has changed.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent)
+ */
+ @Override
+ public void containerItemSetChange(Container.ItemSetChangeEvent event) {
+ // Clears the item id mapping table
+ itemIdMapper.removeAll();
+
+ // Notify all listeners
+ fireItemSetChange();
+ }
+
+ /**
+ * Fires the property set change event.
+ */
+ protected void firePropertySetChange() {
+ if (propertySetEventListeners != null
+ && !propertySetEventListeners.isEmpty()) {
+ final Container.PropertySetChangeEvent event = new PropertySetChangeEvent(
+ this);
+ final Object[] listeners = propertySetEventListeners.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((Container.PropertySetChangeListener) listeners[i])
+ .containerPropertySetChange(event);
+ }
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Fires the item set change event.
+ */
+ protected void fireItemSetChange() {
+ if (itemSetEventListeners != null && !itemSetEventListeners.isEmpty()) {
+ final Container.ItemSetChangeEvent event = new ItemSetChangeEvent(
+ this);
+ final Object[] listeners = itemSetEventListeners.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((Container.ItemSetChangeListener) listeners[i])
+ .containerItemSetChange(event);
+ }
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Implementation of item set change event.
+ */
+ private static class ItemSetChangeEvent extends EventObject implements
+ Serializable, Container.ItemSetChangeEvent {
+
+ private ItemSetChangeEvent(Container source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Property where the event occurred.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeEvent#getContainer()
+ */
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+
+ }
+
+ /**
+ * Implementation of property set change event.
+ */
+ private static class PropertySetChangeEvent extends EventObject implements
+ Container.PropertySetChangeEvent, Serializable {
+
+ private PropertySetChangeEvent(Container source) {
+ super(source);
+ }
+
+ /**
+ * Retrieves the Container whose contents have been modified.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeEvent#getContainer()
+ */
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+
+ }
+
+ /**
+ * For multi-selectable fields, also an empty collection of values is
+ * considered to be an empty field.
+ *
+ * @see AbstractField#isEmpty().
+ */
+ @Override
+ public boolean isEmpty() {
+ if (!multiSelect) {
+ return super.isEmpty();
+ } else {
+ Object value = getValue();
+ return super.isEmpty()
+ || (value instanceof Collection && ((Collection<?>) value)
+ .isEmpty());
+ }
+ }
+
+ /**
+ * Allow or disallow empty selection by the user. If the select is in
+ * single-select mode, you can make an item represent the empty selection by
+ * calling <code>setNullSelectionItemId()</code>. This way you can for
+ * instance set an icon and caption for the null selection item.
+ *
+ * @param nullSelectionAllowed
+ * whether or not to allow empty selection
+ * @see #setNullSelectionItemId(Object)
+ * @see #isNullSelectionAllowed()
+ */
+ public void setNullSelectionAllowed(boolean nullSelectionAllowed) {
+ if (nullSelectionAllowed != this.nullSelectionAllowed) {
+ this.nullSelectionAllowed = nullSelectionAllowed;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Checks if null empty selection is allowed by the user.
+ *
+ * @return whether or not empty selection is allowed
+ * @see #setNullSelectionAllowed(boolean)
+ */
+ public boolean isNullSelectionAllowed() {
+ return nullSelectionAllowed;
+ }
+
+ /**
+ * Returns the item id that represents null value of this select in single
+ * select mode.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ *
+ * @return the Object Null value item id.
+ * @see #setNullSelectionItemId(Object)
+ * @see #isSelected(Object)
+ * @see #select(Object)
+ */
+ public Object getNullSelectionItemId() {
+ return nullSelectionItemId;
+ }
+
+ /**
+ * Sets the item id that represents null value of this select.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ *
+ * @param nullSelectionItemId
+ * the nullSelectionItemId to set.
+ * @see #getNullSelectionItemId()
+ * @see #isSelected(Object)
+ * @see #select(Object)
+ */
+ public void setNullSelectionItemId(Object nullSelectionItemId) {
+ if (nullSelectionItemId != null && isMultiSelect()) {
+ throw new IllegalStateException(
+ "Multiselect and NullSelectionItemId can not be set at the same time.");
+ }
+ this.nullSelectionItemId = nullSelectionItemId;
+ }
+
+ /**
+ * Notifies the component that it is connected to an application.
+ *
+ * @see com.vaadin.ui.AbstractField#attach()
+ */
+ @Override
+ public void attach() {
+ super.attach();
+ }
+
+ /**
+ * Detaches the component from application.
+ *
+ * @see com.vaadin.ui.AbstractComponent#detach()
+ */
+ @Override
+ public void detach() {
+ getCaptionChangeListener().clear();
+ super.detach();
+ }
+
+ // Caption change listener
+ protected CaptionChangeListener getCaptionChangeListener() {
+ if (captionChangeListener == null) {
+ captionChangeListener = new CaptionChangeListener();
+ }
+ return captionChangeListener;
+ }
+
+ /**
+ * This is a listener helper for Item and Property changes that should cause
+ * a repaint. It should be attached to all items that are displayed, and the
+ * default implementation does this in paintContent(). Especially
+ * "lazyloading" components should take care to add and remove listeners as
+ * appropriate. Call addNotifierForItem() for each painted item (and
+ * remember to clear).
+ *
+ * NOTE: singleton, use getCaptionChangeListener().
+ *
+ */
+ protected class CaptionChangeListener implements
+ Item.PropertySetChangeListener, Property.ValueChangeListener {
+
+ // TODO clean this up - type is either Item.PropertySetChangeNotifier or
+ // Property.ValueChangeNotifier
+ HashSet<Object> captionChangeNotifiers = new HashSet<Object>();
+
+ public void addNotifierForItem(Object itemId) {
+ switch (getItemCaptionMode()) {
+ case ITEM:
+ final Item i = getItem(itemId);
+ if (i == null) {
+ return;
+ }
+ if (i instanceof Item.PropertySetChangeNotifier) {
+ ((Item.PropertySetChangeNotifier) i)
+ .addPropertySetChangeListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(i);
+ }
+ Collection<?> pids = i.getItemPropertyIds();
+ if (pids != null) {
+ for (Iterator<?> it = pids.iterator(); it.hasNext();) {
+ Property<?> p = i.getItemProperty(it.next());
+ if (p != null
+ && p instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) p)
+ .addValueChangeListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(p);
+ }
+ }
+
+ }
+ break;
+ case PROPERTY:
+ final Property<?> p = getContainerProperty(itemId,
+ getItemCaptionPropertyId());
+ if (p != null && p instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) p)
+ .addValueChangeListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(p);
+ }
+ break;
+
+ }
+ if (getItemIconPropertyId() != null) {
+ final Property p = getContainerProperty(itemId,
+ getItemIconPropertyId());
+ if (p != null && p instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) p)
+ .addValueChangeListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(p);
+ }
+ }
+ }
+
+ public void clear() {
+ for (Iterator<Object> it = captionChangeNotifiers.iterator(); it
+ .hasNext();) {
+ Object notifier = it.next();
+ if (notifier instanceof Item.PropertySetChangeNotifier) {
+ ((Item.PropertySetChangeNotifier) notifier)
+ .removePropertySetChangeListener(getCaptionChangeListener());
+ } else {
+ ((Property.ValueChangeNotifier) notifier)
+ .removeValueChangeListener(getCaptionChangeListener());
+ }
+ }
+ captionChangeNotifiers.clear();
+ }
+
+ @Override
+ public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
+ markAsDirty();
+ }
+
+ @Override
+ public void itemPropertySetChange(
+ com.vaadin.data.Item.PropertySetChangeEvent event) {
+ markAsDirty();
+ }
+
+ }
+
+ /**
+ * Criterion which accepts a drop only if the drop target is (one of) the
+ * given Item identifier(s). Criterion can be used only on a drop targets
+ * that extends AbstractSelect like {@link Table} and {@link Tree}. The
+ * target and identifiers of valid Items are given in constructor.
+ *
+ * @since 6.3
+ */
+ public static class TargetItemIs extends AbstractItemSetCriterion {
+
+ /**
+ * @param select
+ * the select implementation that is used as a drop target
+ * @param itemId
+ * the identifier(s) that are valid drop locations
+ */
+ public TargetItemIs(AbstractSelect select, Object... itemId) {
+ super(select, itemId);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent
+ .getTargetDetails();
+ if (dropTargetData.getTarget() != select) {
+ return false;
+ }
+ return itemIds.contains(dropTargetData.getItemIdOver());
+ }
+
+ }
+
+ /**
+ * Abstract helper class to implement item id based criterion.
+ *
+ * Note, inner class used not to open itemIdMapper for public access.
+ *
+ * @since 6.3
+ *
+ */
+ private static abstract class AbstractItemSetCriterion extends
+ ClientSideCriterion {
+ protected final Collection<Object> itemIds = new HashSet<Object>();
+ protected AbstractSelect select;
+
+ public AbstractItemSetCriterion(AbstractSelect select, Object... itemId) {
+ if (itemIds == null || select == null) {
+ throw new IllegalArgumentException(
+ "Accepted item identifiers must be accepted.");
+ }
+ Collections.addAll(itemIds, itemId);
+ this.select = select;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ String[] keys = new String[itemIds.size()];
+ int i = 0;
+ for (Object itemId : itemIds) {
+ String key = select.itemIdMapper.key(itemId);
+ keys[i++] = key;
+ }
+ target.addAttribute("keys", keys);
+ target.addAttribute("s", select);
+ }
+
+ }
+
+ /**
+ * This criterion accepts a only a {@link Transferable} that contains given
+ * Item (practically its identifier) from a specific AbstractSelect.
+ *
+ * @since 6.3
+ */
+ public static class AcceptItem extends AbstractItemSetCriterion {
+
+ /**
+ * @param select
+ * the select from which the item id's are checked
+ * @param itemId
+ * the item identifier(s) of the select that are accepted
+ */
+ public AcceptItem(AbstractSelect select, Object... itemId) {
+ super(select, itemId);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ DataBoundTransferable transferable = (DataBoundTransferable) dragEvent
+ .getTransferable();
+ if (transferable.getSourceComponent() != select) {
+ return false;
+ }
+ return itemIds.contains(transferable.getItemId());
+ }
+
+ /**
+ * A simple accept criterion which ensures that {@link Transferable}
+ * contains an {@link Item} (or actually its identifier). In other words
+ * the criterion check that drag is coming from a {@link Container} like
+ * {@link Tree} or {@link Table}.
+ */
+ public static final ClientSideCriterion ALL = new ContainsDataFlavor(
+ "itemId");
+
+ }
+
+ /**
+ * TargetDetails implementation for subclasses of {@link AbstractSelect}
+ * that implement {@link DropTarget}.
+ *
+ * @since 6.3
+ */
+ public class AbstractSelectTargetDetails extends TargetDetailsImpl {
+
+ /**
+ * The item id over which the drag event happened.
+ */
+ protected Object idOver;
+
+ /**
+ * Constructor that automatically converts itemIdOver key to
+ * corresponding item Id
+ *
+ */
+ protected AbstractSelectTargetDetails(Map<String, Object> rawVariables) {
+ super(rawVariables, (DropTarget) AbstractSelect.this);
+ // eagar fetch itemid, mapper may be emptied
+ String keyover = (String) getData("itemIdOver");
+ if (keyover != null) {
+ idOver = itemIdMapper.get(keyover);
+ }
+ }
+
+ /**
+ * If the drag operation is currently over an {@link Item}, this method
+ * returns the identifier of that {@link Item}.
+ *
+ */
+ public Object getItemIdOver() {
+ return idOver;
+ }
+
+ /**
+ * Returns a detailed vertical location where the drop happened on Item.
+ */
+ public VerticalDropLocation getDropLocation() {
+ String detail = (String) getData("detail");
+ if (detail == null) {
+ return null;
+ }
+ return VerticalDropLocation.valueOf(detail);
+ }
+
+ }
+
+ /**
+ * An accept criterion to accept drops only on a specific vertical location
+ * of an item.
+ * <p>
+ * This accept criterion is currently usable in Tree and Table
+ * implementations.
+ */
+ public static class VerticalLocationIs extends TargetDetailIs {
+ public static VerticalLocationIs TOP = new VerticalLocationIs(
+ VerticalDropLocation.TOP);
+ public static VerticalLocationIs BOTTOM = new VerticalLocationIs(
+ VerticalDropLocation.BOTTOM);
+ public static VerticalLocationIs MIDDLE = new VerticalLocationIs(
+ VerticalDropLocation.MIDDLE);
+
+ private VerticalLocationIs(VerticalDropLocation l) {
+ super("detail", l.name());
+ }
+ }
+
+ /**
+ * Implement this interface and pass it to Tree.setItemDescriptionGenerator
+ * or Table.setItemDescriptionGenerator to generate mouse over descriptions
+ * ("tooltips") for the rows and cells in Table or for the items in Tree.
+ */
+ public interface ItemDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Table when a cell (and row) is painted or a item is painted
+ * in Tree
+ *
+ * @param source
+ * The source of the generator, the Tree or Table the
+ * generator is attached to
+ * @param itemId
+ * The itemId of the painted cell
+ * @param propertyId
+ * The propertyId of the cell, null when getting row
+ * description
+ * @return The description or "tooltip" of the item.
+ */
+ public String generateDescription(Component source, Object itemId,
+ Object propertyId);
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext context) {
+ // handle default attributes
+ super.readDesign(design, context);
+ // handle children specifying selectable items (<option>)
+ readItems(design, context);
+ }
+
+ protected void readItems(Element design, DesignContext context) {
+ Set<String> selected = new HashSet<String>();
+ for (Element child : design.children()) {
+ readItem(child, selected, context);
+ }
+ if (!selected.isEmpty()) {
+ if (isMultiSelect()) {
+ setValue(selected, false, true);
+ } else if (selected.size() == 1) {
+ setValue(selected.iterator().next(), false, true);
+ } else {
+ throw new DesignException(
+ "Multiple values selected for a single select component");
+ }
+ }
+ }
+
+ /**
+ * Reads an Item from a design and inserts it into the data source.
+ * Hierarchical select components should override this method to recursively
+ * recursively read any child items as well.
+ *
+ * @since 7.5.0
+ * @param child
+ * a child element representing the item
+ * @param selected
+ * A set accumulating selected items. If the item that is read is
+ * marked as selected, its item id should be added to this set.
+ * @param context
+ * the DesignContext instance used in parsing
+ * @return the item id of the new item
+ *
+ * @throws DesignException
+ * if the tag name of the {@code child} element is not
+ * {@code option}.
+ */
+ protected Object readItem(Element child, Set<String> selected,
+ DesignContext context) {
+ if (!"option".equals(child.tagName())) {
+ throw new DesignException("Unrecognized child element in "
+ + getClass().getSimpleName() + ": " + child.tagName());
+ }
+
+ String itemId;
+ String caption = DesignFormatter.decodeFromTextNode(child.html());
+ if (child.hasAttr("item-id")) {
+ itemId = child.attr("item-id");
+ addItem(itemId);
+ setItemCaption(itemId, caption);
+ } else {
+ addItem(itemId = caption);
+ }
+
+ if (child.hasAttr("icon")) {
+ setItemIcon(
+ itemId,
+ DesignAttributeHandler.readAttribute("icon",
+ child.attributes(), Resource.class));
+ }
+
+ if (child.hasAttr("selected")) {
+ selected.add(itemId);
+ }
+
+ return itemId;
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext context) {
+ // Write default attributes
+ super.writeDesign(design, context);
+
+ // Write options if warranted
+ if (context.shouldWriteData(this)) {
+ writeItems(design, context);
+ }
+ }
+
+ /**
+ * Writes the data source items to a design. Hierarchical select components
+ * should override this method to only write the root items.
+ *
+ * @since 7.5.0
+ * @param design
+ * the element into which to insert the items
+ * @param context
+ * the DesignContext instance used in writing
+ */
+ protected void writeItems(Element design, DesignContext context) {
+ for (Object itemId : getItemIds()) {
+ writeItem(design, itemId, context);
+ }
+ }
+
+ /**
+ * Writes a data source Item to a design. Hierarchical select components
+ * should override this method to recursively write any child items as well.
+ *
+ * @since 7.5.0
+ * @param design
+ * the element into which to insert the item
+ * @param itemId
+ * the id of the item to write
+ * @param context
+ * the DesignContext instance used in writing
+ * @return
+ */
+ protected Element writeItem(Element design, Object itemId,
+ DesignContext context) {
+ Element element = design.appendElement("option");
+
+ String caption = getItemCaption(itemId);
+ if (caption != null && !caption.equals(itemId.toString())) {
+ element.html(DesignFormatter.encodeForTextNode(caption));
+ element.attr("item-id", itemId.toString());
+ } else {
+ element.html(DesignFormatter.encodeForTextNode(itemId.toString()));
+ }
+
+ Resource icon = getItemIcon(itemId);
+ if (icon != null) {
+ DesignAttributeHandler.writeAttribute("icon", element.attributes(),
+ icon, null, Resource.class);
+ }
+
+ if (isSelected(itemId)) {
+ element.attr("selected", "");
+ }
+
+ return element;
+ }
+
+ @Override
+ protected AbstractSelectState getState() {
+ return (AbstractSelectState) super.getState();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.ui;
+
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.jsoup.nodes.Element;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.TransferableImpl;
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetails;
+import com.vaadin.event.dd.TargetDetailsImpl;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.StreamVariable;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.dd.HorizontalDropLocation;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.shared.ui.draganddropwrapper.DragAndDropWrapperConstants;
++import com.vaadin.shared.ui.draganddropwrapper.DragAndDropWrapperServerRpc;
+import com.vaadin.ui.declarative.DesignContext;
+
+@SuppressWarnings("serial")
+public class DragAndDropWrapper extends CustomComponent implements DropTarget,
+ DragSource, LegacyComponent {
+
+ public class WrapperTransferable extends TransferableImpl {
+
+ private Html5File[] files;
+
+ public WrapperTransferable(Component sourceComponent,
+ Map<String, Object> rawVariables) {
+ super(sourceComponent, rawVariables);
+ Integer fc = (Integer) rawVariables.get("filecount");
+ if (fc != null) {
+ files = new Html5File[fc];
+ for (int i = 0; i < fc; i++) {
+ Html5File file = new Html5File(
+ (String) rawVariables.get("fn" + i), // name
+ ((Double) rawVariables.get("fs" + i)).longValue(), // size
+ (String) rawVariables.get("ft" + i)); // mime
+ String id = (String) rawVariables.get("fi" + i);
+ files[i] = file;
+ receivers.put(id, new ProxyReceiver(id, file));
+ markAsDirty(); // paint Receivers
+ }
+ }
+ }
+
+ /**
+ * The component in wrapper that is being dragged or null if the
+ * transferable is not a component (most likely an html5 drag).
+ *
+ * @return
+ */
+ public Component getDraggedComponent() {
+ Component object = (Component) getData("component");
+ return object;
+ }
+
+ /**
+ * @return the mouse down event that started the drag and drop operation
+ */
+ public MouseEventDetails getMouseDownEvent() {
+ return MouseEventDetails.deSerialize((String) getData("mouseDown"));
+ }
+
+ public Html5File[] getFiles() {
+ return files;
+ }
+
+ public String getText() {
+ String data = (String) getData("Text"); // IE, html5
+ if (data == null) {
+ // check for "text/plain" (webkit)
+ data = (String) getData("text/plain");
+ }
+ return data;
+ }
+
+ public String getHtml() {
+ String data = (String) getData("Html"); // IE, html5
+ if (data == null) {
+ // check for "text/plain" (webkit)
+ data = (String) getData("text/html");
+ }
+ return data;
+ }
+
+ }
+
++ private final DragAndDropWrapperServerRpc rpc = new DragAndDropWrapperServerRpc() {
++
++ @Override
++ public void poll() {
++ // #19616 RPC to poll the server for changes
++ }
++ };
++
+ private Map<String, ProxyReceiver> receivers = new HashMap<String, ProxyReceiver>();
+
+ public class WrapperTargetDetails extends TargetDetailsImpl {
+
+ public WrapperTargetDetails(Map<String, Object> rawDropData) {
+ super(rawDropData, DragAndDropWrapper.this);
+ }
+
+ /**
+ * @return the absolute position of wrapper on the page
+ */
+ public Integer getAbsoluteLeft() {
+ return (Integer) getData("absoluteLeft");
+ }
+
+ /**
+ *
+ * @return the absolute position of wrapper on the page
+ */
+ public Integer getAbsoluteTop() {
+ return (Integer) getData("absoluteTop");
+ }
+
+ /**
+ * @return a detail about the drags vertical position over the wrapper.
+ */
+ public VerticalDropLocation getVerticalDropLocation() {
+ return VerticalDropLocation
+ .valueOf((String) getData("verticalLocation"));
+ }
+
+ /**
+ * @return a detail about the drags horizontal position over the
+ * wrapper.
+ */
+ public HorizontalDropLocation getHorizontalDropLocation() {
+ return HorizontalDropLocation
+ .valueOf((String) getData("horizontalLocation"));
+ }
+
+ }
+
+ public enum DragStartMode {
+ /**
+ * {@link DragAndDropWrapper} does not start drag events at all
+ */
+ NONE,
+ /**
+ * The component on which the drag started will be shown as drag image.
+ */
+ COMPONENT,
+ /**
+ * The whole wrapper is used as a drag image when dragging.
+ */
+ WRAPPER,
+ /**
+ * The whole wrapper is used to start an HTML5 drag.
+ *
+ * NOTE: In Internet Explorer 6 to 8, this prevents user interactions
+ * with the wrapper's contents. For example, clicking a button inside
+ * the wrapper will no longer work.
+ */
+ HTML5,
+
+ /**
+ * Uses the component defined in
+ * {@link #setDragImageComponent(Component)} as the drag image.
+ */
+ COMPONENT_OTHER,
+ }
+
+ private final Map<String, Object> html5DataFlavors = new LinkedHashMap<String, Object>();
+ private DragStartMode dragStartMode = DragStartMode.NONE;
+ private Component dragImageComponent = null;
+
+ private Set<String> sentIds = new HashSet<String>();
+
+ /**
+ * This is an internal constructor. Use
+ * {@link DragAndDropWrapper#DragAndDropWrapper(Component)} instead.
+ *
+ * @since 7.5.0
+ */
+ @Deprecated
+ public DragAndDropWrapper() {
+ super();
++ registerRpc(rpc);
+ }
+
+ /**
+ * Wraps given component in a {@link DragAndDropWrapper}.
+ *
+ * @param root
+ * the component to be wrapped
+ */
+ public DragAndDropWrapper(Component root) {
+ this();
+ setCompositionRoot(root);
+ }
+
+ /**
+ * Sets data flavors available in the DragAndDropWrapper is used to start an
+ * HTML5 style drags. Most commonly the "Text" flavor should be set.
+ * Multiple data types can be set.
+ *
+ * @param type
+ * the string identifier of the drag "payload". E.g. "Text" or
+ * "text/html"
+ * @param value
+ * the value
+ */
+ public void setHTML5DataFlavor(String type, Object value) {
+ html5DataFlavors.put(type, value);
+ markAsDirty();
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // TODO Remove once LegacyComponent is no longer implemented
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ target.addAttribute(DragAndDropWrapperConstants.DRAG_START_MODE,
+ dragStartMode.ordinal());
+
+ if (dragStartMode.equals(DragStartMode.COMPONENT_OTHER)) {
+ if (dragImageComponent != null) {
+ target.addAttribute(
+ DragAndDropWrapperConstants.DRAG_START_COMPONENT_ATTRIBUTE,
+ dragImageComponent.getConnectorId());
+ } else {
+ throw new IllegalArgumentException(
+ "DragStartMode.COMPONENT_OTHER set but no component "
+ + "was defined. Please set a component using DragAnd"
+ + "DropWrapper.setDragStartComponent(Component).");
+ }
+ }
+ if (getDropHandler() != null) {
+ getDropHandler().getAcceptCriterion().paint(target);
+ }
+ if (receivers != null && receivers.size() > 0) {
+ for (Iterator<Entry<String, ProxyReceiver>> it = receivers
+ .entrySet().iterator(); it.hasNext();) {
+ Entry<String, ProxyReceiver> entry = it.next();
+ String id = entry.getKey();
+ ProxyReceiver proxyReceiver = entry.getValue();
+ Html5File html5File = proxyReceiver.file;
+ if (html5File.getStreamVariable() != null) {
+ if (!sentIds.contains(id)) {
+ target.addVariable(this, "rec-" + id,
+ new ProxyReceiver(id, html5File));
+
+ /*
+ * if a new batch is requested to be uploaded before the
+ * last one is done, any remaining ids will be replayed.
+ * We want to avoid a new ProxyReceiver to be made since
+ * it'll get a new URL, so we need to keep extra track
+ * on what has been sent.
+ *
+ * See #12330.
+ */
+ sentIds.add(id);
+
+ // these are cleaned from receivers once the upload has
+ // started
+ }
+ } else {
+ // instructs the client side not to send the file
+ target.addVariable(this, "rec-" + id, (String) null);
+ // forget the file from subsequent paints
+ it.remove();
+ }
+ }
+ }
+ target.addAttribute(DragAndDropWrapperConstants.HTML5_DATA_FLAVORS,
+ html5DataFlavors);
+ }
+
+ private DropHandler dropHandler;
+
+ @Override
+ public DropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ public void setDropHandler(DropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ markAsDirty();
+ }
+
+ @Override
+ public TargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables) {
+ return new WrapperTargetDetails(clientVariables);
+ }
+
+ @Override
+ public Transferable getTransferable(final Map<String, Object> rawVariables) {
+ return new WrapperTransferable(this, rawVariables);
+ }
+
+ public void setDragStartMode(DragStartMode dragStartMode) {
+ this.dragStartMode = dragStartMode;
+ markAsDirty();
+ }
+
+ public DragStartMode getDragStartMode() {
+ return dragStartMode;
+ }
+
+ /**
+ * Sets the component that will be used as the drag image. Only used when
+ * wrapper is set to {@link DragStartMode#COMPONENT_OTHER}
+ *
+ * @param dragImageComponent
+ */
+ public void setDragImageComponent(Component dragImageComponent) {
+ this.dragImageComponent = dragImageComponent;
+ markAsDirty();
+ }
+
+ /**
+ * Gets the component that will be used as the drag image. Only used when
+ * wrapper is set to {@link DragStartMode#COMPONENT_OTHER}
+ *
+ * @return <code>null</code> if no component is set.
+ */
+ public Component getDragImageComponent() {
+ return dragImageComponent;
+ }
+
+ final class ProxyReceiver implements StreamVariable {
+
+ private String id;
+ private Html5File file;
+
+ public ProxyReceiver(String id, Html5File file) {
+ this.id = id;
+ this.file = file;
+ }
+
+ private boolean listenProgressOfUploadedFile;
+
+ @Override
+ public OutputStream getOutputStream() {
+ if (file.getStreamVariable() == null) {
+ return null;
+ }
+ return file.getStreamVariable().getOutputStream();
+ }
+
+ @Override
+ public boolean listenProgress() {
+ return file.getStreamVariable().listenProgress();
+ }
+
+ @Override
+ public void onProgress(StreamingProgressEvent event) {
+ file.getStreamVariable().onProgress(
+ new ReceivingEventWrapper(event));
+ }
+
+ @Override
+ public void streamingStarted(StreamingStartEvent event) {
+ listenProgressOfUploadedFile = file.getStreamVariable() != null;
+ if (listenProgressOfUploadedFile) {
+ file.getStreamVariable().streamingStarted(
+ new ReceivingEventWrapper(event));
+ }
+ // no need tell to the client about this receiver on next paint
+ receivers.remove(id);
+ sentIds.remove(id);
+ // let the terminal GC the streamvariable and not to accept other
+ // file uploads to this variable
+ event.disposeStreamVariable();
+ }
+
+ @Override
+ public void streamingFinished(StreamingEndEvent event) {
+ if (listenProgressOfUploadedFile) {
+ file.getStreamVariable().streamingFinished(
+ new ReceivingEventWrapper(event));
+ }
+ }
+
+ @Override
+ public void streamingFailed(final StreamingErrorEvent event) {
+ if (listenProgressOfUploadedFile) {
+ file.getStreamVariable().streamingFailed(
+ new ReceivingEventWrapper(event));
+ }
+ }
+
+ @Override
+ public boolean isInterrupted() {
+ return file.getStreamVariable().isInterrupted();
+ }
+
+ /*
+ * With XHR2 file posts we can't provide as much information from the
+ * terminal as with multipart request. This helper class wraps the
+ * terminal event and provides the lacking information from the
+ * Html5File.
+ */
+ class ReceivingEventWrapper implements StreamingErrorEvent,
+ StreamingEndEvent, StreamingStartEvent, StreamingProgressEvent {
+
+ private StreamingEvent wrappedEvent;
+
+ ReceivingEventWrapper(StreamingEvent e) {
+ wrappedEvent = e;
+ }
+
+ @Override
+ public String getMimeType() {
+ return file.getType();
+ }
+
+ @Override
+ public String getFileName() {
+ return file.getFileName();
+ }
+
+ @Override
+ public long getContentLength() {
+ return file.getFileSize();
+ }
+
+ public StreamVariable getReceiver() {
+ return ProxyReceiver.this;
+ }
+
+ @Override
+ public Exception getException() {
+ if (wrappedEvent instanceof StreamingErrorEvent) {
+ return ((StreamingErrorEvent) wrappedEvent).getException();
+ }
+ return null;
+ }
+
+ @Override
+ public long getBytesReceived() {
+ return wrappedEvent.getBytesReceived();
+ }
+
+ /**
+ * Calling this method has no effect. DD files are receive only once
+ * anyway.
+ */
+ @Override
+ public void disposeStreamVariable() {
+
+ }
+ }
+
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+
+ for (Element child : design.children()) {
+ Component component = designContext.readDesign(child);
+ if (getDragStartMode() == DragStartMode.COMPONENT_OTHER
+ && child.hasAttr(":drag-image")) {
+ setDragImageComponent(component);
+ } else if (getCompositionRoot() == null) {
+ setCompositionRoot(component);
+ }
+ }
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+
+ design.appendChild(designContext.createElement(getCompositionRoot()));
+ if (getDragStartMode() == DragStartMode.COMPONENT_OTHER) {
+ Element child = designContext
+ .createElement(getDragImageComponent());
+ child.attr(":drag-image", true);
+ design.appendChild(child);
+ }
+ }
+}
--- /dev/null
- setMargin(new MarginInfo(true, false, true, false));
+/*
+ * Copyright 2000-2014 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.ui;
+
+import com.vaadin.shared.ui.MarginInfo;
+import com.vaadin.shared.ui.orderedlayout.FormLayoutState;
+
+/**
+ * FormLayout is used by {@link Form} to layout fields. It may also be used
+ * separately without {@link Form}.
+ *
+ * FormLayout is a close relative of {@link VerticalLayout}, but in FormLayout
+ * captions are rendered to the left of their respective components. Required
+ * and validation indicators are shown between the captions and the fields.
+ *
+ * FormLayout by default has component spacing on. Also margin top and margin
+ * bottom are by default on.
+ *
+ */
+public class FormLayout extends AbstractOrderedLayout {
+
+ public FormLayout() {
+ super();
+ setSpacing(true);
++ setMargin(new MarginInfo(true, false));
+ setWidth(100, UNITS_PERCENTAGE);
+ }
+
+ /**
+ * Constructs a FormLayout and adds the given components to it.
+ *
+ * @see AbstractOrderedLayout#addComponents(Component...)
+ *
+ * @param children
+ * Components to add to the FormLayout
+ */
+ public FormLayout(Component... children) {
+ this();
+ addComponents(children);
+ }
+
+ /**
+ * @deprecated This method currently has no effect as expand ratios are not
+ * implemented in FormLayout
+ */
+ @Override
+ @Deprecated
+ public void setExpandRatio(Component component, float ratio) {
+ super.setExpandRatio(component, ratio);
+ }
+
+ /**
+ * @deprecated This method currently has no effect as expand ratios are not
+ * implemented in FormLayout
+ */
+ @Override
+ @Deprecated
+ public float getExpandRatio(Component component) {
+ return super.getExpandRatio(component);
+ }
+
+ @Override
+ protected FormLayoutState getState() {
+ return (FormLayoutState) super.getState();
+ }
+}
--- /dev/null
- * Sets the grid data source.
- *
+/*
+ * Copyright 2000-2014 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.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
+import com.vaadin.data.Container.PropertySetChangeEvent;
+import com.vaadin.data.Container.PropertySetChangeListener;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Validator.InvalidValueException;
+import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.fieldgroup.FieldGroupFieldFactory;
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.event.ContextClickEvent;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.event.SelectionEvent.SelectionNotifier;
+import com.vaadin.event.SortEvent;
+import com.vaadin.event.SortEvent.SortListener;
+import com.vaadin.event.SortEvent.SortNotifier;
+import com.vaadin.server.AbstractClientConnector;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.EncodeResult;
+import com.vaadin.server.ErrorMessage;
+import com.vaadin.server.Extension;
+import com.vaadin.server.JsonCodec;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.server.communication.data.DataGenerator;
+import com.vaadin.server.communication.data.RpcDataProviderExtension;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.EditorClientRpc;
+import com.vaadin.shared.ui.grid.EditorServerRpc;
+import com.vaadin.shared.ui.grid.GridClientRpc;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.shared.ui.grid.GridServerRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.ui.grid.selection.MultiSelectionModelServerRpc;
+import com.vaadin.shared.ui.grid.selection.MultiSelectionModelState;
+import com.vaadin.shared.ui.grid.selection.SingleSelectionModelServerRpc;
+import com.vaadin.shared.ui.grid.selection.SingleSelectionModelState;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.ui.declarative.DesignFormatter;
+import com.vaadin.ui.renderers.HtmlRenderer;
+import com.vaadin.ui.renderers.Renderer;
+import com.vaadin.ui.renderers.TextRenderer;
+import com.vaadin.util.ReflectTools;
+
+import elemental.json.Json;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * A grid component for displaying tabular data.
+ * <p>
+ * Grid is always bound to a {@link Container.Indexed}, but is not a
+ * {@code Container} of any kind in of itself. The contents of the given
+ * Container is displayed with the help of {@link Renderer Renderers}.
+ *
+ * <h3 id="grid-headers-and-footers">Headers and Footers</h3>
+ * <p>
+ *
+ *
+ * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3>
+ * <p>
+ * Each column has its own {@link Renderer} that displays data into something
+ * that can be displayed in the browser. That data is first converted with a
+ * {@link com.vaadin.data.util.converter.Converter Converter} into something
+ * that the Renderer can process. This can also be an implicit step - if a
+ * column has a simple data type, like a String, no explicit assignment is
+ * needed.
+ * <p>
+ * Usually a renderer takes some kind of object, and converts it into a
+ * HTML-formatted string.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ * Column column = grid.getColumn(STRING_DATE_PROPERTY);
+ * column.setConverter(new StringToDateConverter());
+ * column.setRenderer(new MyColorfulDateRenderer());
+ * </pre></code>
+ *
+ * <h3 id="grid-lazyloading">Lazy Loading</h3>
+ * <p>
+ * The data is accessed as it is needed by Grid and not any sooner. In other
+ * words, if the given Container is huge, but only the first few rows are
+ * displayed to the user, only those (and a few more, for caching purposes) are
+ * accessed.
+ *
+ * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3>
+ * <p>
+ * Grid supports three selection <em>{@link SelectionMode modes}</em> (single,
+ * multi, none), and comes bundled with one
+ * <em>{@link SelectionModel model}</em> for each of the modes. The distinction
+ * between a selection mode and selection model is as follows: a <em>mode</em>
+ * essentially says whether you can have one, many or no rows selected. The
+ * model, however, has the behavioral details of each. A single selection model
+ * may require that the user deselects one row before selecting another one. A
+ * variant of a multiselect might have a configurable maximum of rows that may
+ * be selected. And so on.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ *
+ * // uses the bundled SingleSelectionModel class
+ * grid.setSelectionMode(SelectionMode.SINGLE);
+ *
+ * // changes the behavior to a custom selection model
+ * grid.setSelectionModel(new MyTwoSelectionModel());
+ * </pre></code>
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Grid extends AbstractFocusable implements SelectionNotifier,
+ SortNotifier, SelectiveRenderer, ItemClickNotifier {
+
+ /**
+ * An event listener for column visibility change events in the Grid.
+ *
+ * @since 7.5.0
+ */
+ public interface ColumnVisibilityChangeListener extends Serializable {
+ /**
+ * Called when a column has become hidden or unhidden.
+ *
+ * @param event
+ */
+ void columnVisibilityChanged(ColumnVisibilityChangeEvent event);
+ }
+
+ /**
+ * An event that is fired when a column's visibility changes.
+ *
+ * @since 7.5.0
+ */
+ public static class ColumnVisibilityChangeEvent extends Component.Event {
+
+ private final Column column;
+ private final boolean userOriginated;
+ private final boolean hidden;
+
+ /**
+ * Constructor for a column visibility change event.
+ *
+ * @param source
+ * the grid from which this event originates
+ * @param column
+ * the column that changed its visibility
+ * @param hidden
+ * <code>true</code> if the column was hidden,
+ * <code>false</code> if it became visible
+ * @param isUserOriginated
+ * <code>true</code> iff the event was triggered by an UI
+ * interaction
+ */
+ public ColumnVisibilityChangeEvent(Grid source, Column column,
+ boolean hidden, boolean isUserOriginated) {
+ super(source);
+ this.column = column;
+ this.hidden = hidden;
+ userOriginated = isUserOriginated;
+ }
+
+ /**
+ * Gets the column that became hidden or visible.
+ *
+ * @return the column that became hidden or visible.
+ * @see Column#isHidden()
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Was the column set hidden or visible.
+ *
+ * @return <code>true</code> if the column was hidden <code>false</code>
+ * if it was set visible
+ */
+ public boolean isHidden() {
+ return hidden;
+ }
+
+ /**
+ * Returns <code>true</code> if the column reorder was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+ }
+
+ /**
+ * A callback interface for generating details for a particular row in Grid.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ * @see DetailsGenerator#NULL
+ */
+ public interface DetailsGenerator extends Serializable {
+
+ /** A details generator that provides no details */
+ public DetailsGenerator NULL = new DetailsGenerator() {
+ @Override
+ public Component getDetails(RowReference rowReference) {
+ return null;
+ }
+ };
+
+ /**
+ * This method is called for whenever a details row needs to be shown on
+ * the client. Grid removes all of its references to details components
+ * when they are no longer displayed on the client-side and will
+ * re-request once needed again.
+ * <p>
+ * <em>Note:</em> If a component gets generated, it may not be manually
+ * attached anywhere. The same details component can not be displayed
+ * for multiple different rows.
+ *
+ * @param rowReference
+ * the reference for the row for which to generate details
+ * @return the details for the given row, or <code>null</code> to leave
+ * the details empty.
+ */
+ Component getDetails(RowReference rowReference);
+ }
+
+ /**
+ * A class that manages details components by calling
+ * {@link DetailsGenerator} as needed. Details components are attached by
+ * this class when the {@link RpcDataProviderExtension} is sending data to
+ * the client. Details components are detached and forgotten when client
+ * informs that it has dropped the corresponding item.
+ *
+ * @since 7.6.1
+ */
+ public final static class DetailComponentManager extends
+ AbstractGridExtension implements DataGenerator {
+
+ /**
+ * The user-defined details generator.
+ *
+ * @see #setDetailsGenerator(DetailsGenerator)
+ */
+ private DetailsGenerator detailsGenerator;
+
+ /**
+ * This map represents all details that are currently visible on the
+ * client. Details components get destroyed once they scroll out of
+ * view.
+ */
+ private final Map<Object, Component> itemIdToDetailsComponent = new HashMap<Object, Component>();
+
+ /**
+ * Set of item ids that got <code>null</code> from DetailsGenerator when
+ * {@link DetailsGenerator#getDetails(RowReference)} was called.
+ */
+ private final Set<Object> emptyDetails = new HashSet<Object>();
+
+ /**
+ * Set of item IDs for all open details rows. Contains even the ones
+ * that are not currently visible on the client.
+ */
+ private final Set<Object> openDetails = new HashSet<Object>();
+
+ public DetailComponentManager(Grid grid) {
+ this(grid, DetailsGenerator.NULL);
+ }
+
+ public DetailComponentManager(Grid grid,
+ DetailsGenerator detailsGenerator) {
+ super(grid);
+ setDetailsGenerator(detailsGenerator);
+ }
+
+ /**
+ * Creates a details component with the help of the user-defined
+ * {@link DetailsGenerator}.
+ * <p>
+ * This method attaches created components to the parent {@link Grid}.
+ *
+ * @param itemId
+ * the item id for which to create the details component.
+ * @throws IllegalStateException
+ * if the current details generator provides a component
+ * that was manually attached.
+ */
+ private void createDetails(Object itemId) throws IllegalStateException {
+ assert itemId != null : "itemId was null";
+
+ if (itemIdToDetailsComponent.containsKey(itemId)
+ || emptyDetails.contains(itemId)) {
+ // Don't overwrite existing components
+ return;
+ }
+
+ RowReference rowReference = new RowReference(getParentGrid());
+ rowReference.set(itemId);
+
+ DetailsGenerator detailsGenerator = getParentGrid()
+ .getDetailsGenerator();
+ Component details = detailsGenerator.getDetails(rowReference);
+ if (details != null) {
+ if (details.getParent() != null) {
+ String name = detailsGenerator.getClass().getName();
+ throw new IllegalStateException(name
+ + " generated a details component that already "
+ + "was attached. (itemId: " + itemId
+ + ", component: " + details + ")");
+ }
+
+ itemIdToDetailsComponent.put(itemId, details);
+
+ addComponentToGrid(details);
+
+ assert !emptyDetails.contains(itemId) : "Bookeeping thinks "
+ + "itemId is empty even though we just created a "
+ + "component for it (" + itemId + ")";
+ } else {
+ emptyDetails.add(itemId);
+ }
+
+ }
+
+ /**
+ * Destroys a details component correctly.
+ * <p>
+ * This method will detach the component from parent {@link Grid}.
+ *
+ * @param itemId
+ * the item id for which to destroy the details component
+ */
+ private void destroyDetails(Object itemId) {
+ emptyDetails.remove(itemId);
+
+ Component removedComponent = itemIdToDetailsComponent
+ .remove(itemId);
+ if (removedComponent == null) {
+ return;
+ }
+
+ removeComponentFromGrid(removedComponent);
+ }
+
+ /**
+ * Recreates all visible details components.
+ */
+ public void refreshDetails() {
+ Set<Object> visibleItemIds = new HashSet<Object>(
+ itemIdToDetailsComponent.keySet());
+ for (Object itemId : visibleItemIds) {
+ destroyDetails(itemId);
+ createDetails(itemId);
+ refreshRow(itemId);
+ }
+ }
+
+ /**
+ * Sets details visiblity status of given item id.
+ *
+ * @param itemId
+ * item id to set
+ * @param visible
+ * <code>true</code> if visible; <code>false</code> if not
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ if ((visible && openDetails.contains(itemId))
+ || (!visible && !openDetails.contains(itemId))) {
+ return;
+ }
+
+ if (visible) {
+ openDetails.add(itemId);
+ refreshRow(itemId);
+ } else {
+ openDetails.remove(itemId);
+ destroyDetails(itemId);
+ refreshRow(itemId);
+ }
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ // DetailComponentManager should not send anything if details
+ // generator is the default null version.
+ if (openDetails.contains(itemId)
+ && !detailsGenerator.equals(DetailsGenerator.NULL)) {
+ // Double check to be sure details component exists.
+ createDetails(itemId);
+
+ Component detailsComponent = itemIdToDetailsComponent
+ .get(itemId);
+ rowData.put(
+ GridState.JSONKEY_DETAILS_VISIBLE,
+ (detailsComponent != null ? detailsComponent
+ .getConnectorId() : ""));
+ }
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ if (openDetails.contains(itemId)) {
+ destroyDetails(itemId);
+ }
+ }
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+ if (detailsGenerator == null) {
+ throw new IllegalArgumentException(
+ "Details generator may not be null");
+ } else if (detailsGenerator == this.detailsGenerator) {
+ return;
+ }
+
+ this.detailsGenerator = detailsGenerator;
+
+ refreshDetails();
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailsGenerator;
+ }
+
+ /**
+ * Checks whether details are visible for the given item.
+ *
+ * @param itemId
+ * the id of the item for which to check details visibility
+ * @return <code>true</code> iff the details are visible
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return openDetails.contains(itemId);
+ }
+ }
+
+ /**
+ * Custom field group that allows finding property types before an item has
+ * been bound.
+ */
+ private final class CustomFieldGroup extends FieldGroup {
+
+ public CustomFieldGroup() {
+ setFieldFactory(EditorFieldFactory.get());
+ }
+
+ @Override
+ protected Class<?> getPropertyType(Object propertyId)
+ throws BindException {
+ if (getItemDataSource() == null) {
+ return datasource.getType(propertyId);
+ } else {
+ return super.getPropertyType(propertyId);
+ }
+ }
+
+ @Override
+ protected <T extends Field> T build(String caption, Class<?> dataType,
+ Class<T> fieldType) throws BindException {
+ T field = super.build(caption, dataType, fieldType);
+ if (field instanceof CheckBox) {
+ field.setCaption(null);
+ }
+ return field;
+ }
+ }
+
+ /**
+ * Field factory used by default in the editor.
+ *
+ * Aims to fields of suitable type and with suitable size for use in the
+ * editor row.
+ */
+ public static class EditorFieldFactory extends
+ DefaultFieldGroupFieldFactory {
+ private static final EditorFieldFactory INSTANCE = new EditorFieldFactory();
+
+ protected EditorFieldFactory() {
+ }
+
+ /**
+ * Returns the singleton instance
+ *
+ * @return the singleton instance
+ */
+ public static EditorFieldFactory get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public <T extends Field> T createField(Class<?> type, Class<T> fieldType) {
+ T f = super.createField(type, fieldType);
+ if (f != null) {
+ f.setWidth("100%");
+ }
+ return f;
+ }
+
+ @Override
+ protected AbstractSelect createCompatibleSelect(
+ Class<? extends AbstractSelect> fieldType) {
+ if (anySelect(fieldType)) {
+ return super.createCompatibleSelect(ComboBox.class);
+ }
+ return super.createCompatibleSelect(fieldType);
+ }
+
+ @Override
+ protected void populateWithEnumData(AbstractSelect select,
+ Class<? extends Enum> enumClass) {
+ // Use enums directly and the EnumToStringConverter to be consistent
+ // with what is shown in the Grid
+ @SuppressWarnings("unchecked")
+ EnumSet<?> enumSet = EnumSet.allOf(enumClass);
+ for (Object r : enumSet) {
+ select.addItem(r);
+ }
+ }
+ }
+
+ /**
+ * Error handler for the editor
+ */
+ public interface EditorErrorHandler extends Serializable {
+
+ /**
+ * Called when an exception occurs while the editor row is being saved
+ *
+ * @param event
+ * An event providing more information about the error
+ */
+ void commitError(CommitErrorEvent event);
+ }
+
+ /**
+ * ContextClickEvent for the Grid Component.
+ *
+ * @since 7.6
+ */
+ public static class GridContextClickEvent extends ContextClickEvent {
+
+ private final Object itemId;
+ private final int rowIndex;
+ private final Object propertyId;
+ private final Section section;
+
+ public GridContextClickEvent(Grid source,
+ MouseEventDetails mouseEventDetails, Section section,
+ int rowIndex, Object itemId, Object propertyId) {
+ super(source, mouseEventDetails);
+ this.itemId = itemId;
+ this.propertyId = propertyId;
+ this.section = section;
+ this.rowIndex = rowIndex;
+ }
+
+ /**
+ * Returns the item id of context clicked row.
+ *
+ * @return item id of clicked row; <code>null</code> if header or footer
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Returns property id of clicked column.
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Return the clicked section of Grid.
+ *
+ * @return section of grid
+ */
+ public Section getSection() {
+ return section;
+ }
+
+ /**
+ * Returns the clicked row index relative to Grid section. In the body
+ * of the Grid the index is the item index in the Container. Header and
+ * Footer rows for index can be fetched with
+ * {@link Grid#getHeaderRow(int)} and {@link Grid#getFooterRow(int)}.
+ *
+ * @return row index in section
+ */
+ public int getRowIndex() {
+ return rowIndex;
+ }
+
+ @Override
+ public Grid getComponent() {
+ return (Grid) super.getComponent();
+ }
+ }
+
+ /**
+ * An event which is fired when saving the editor fails
+ */
+ public static class CommitErrorEvent extends Component.Event {
+
+ private CommitException cause;
+
+ private Set<Column> errorColumns = new HashSet<Column>();
+
+ private String userErrorMessage;
+
+ public CommitErrorEvent(Grid grid, CommitException cause) {
+ super(grid);
+ this.cause = cause;
+ userErrorMessage = cause.getLocalizedMessage();
+ }
+
+ /**
+ * Retrieves the cause of the failure
+ *
+ * @return the cause of the failure
+ */
+ public CommitException getCause() {
+ return cause;
+ }
+
+ @Override
+ public Grid getComponent() {
+ return (Grid) super.getComponent();
+ }
+
+ /**
+ * Checks if validation exceptions caused this error
+ *
+ * @return true if the problem was caused by a validation error
+ */
+ public boolean isValidationFailure() {
+ return cause.getCause() instanceof InvalidValueException;
+ }
+
+ /**
+ * Marks that an error indicator should be shown for the editor of a
+ * column.
+ *
+ * @param column
+ * the column to show an error for
+ */
+ public void addErrorColumn(Column column) {
+ errorColumns.add(column);
+ }
+
+ /**
+ * Gets all the columns that have been marked as erroneous.
+ *
+ * @return an umodifiable collection of erroneous columns
+ */
+ public Collection<Column> getErrorColumns() {
+ return Collections.unmodifiableCollection(errorColumns);
+ }
+
+ /**
+ * Gets the error message to show to the user.
+ *
+ * @return error message to show
+ */
+ public String getUserErrorMessage() {
+ return userErrorMessage;
+ }
+
+ /**
+ * Sets the error message to show to the user.
+ *
+ * @param userErrorMessage
+ * the user error message to set
+ */
+ public void setUserErrorMessage(String userErrorMessage) {
+ this.userErrorMessage = userErrorMessage;
+ }
+
+ }
+
+ /**
+ * An event listener for column reorder events in the Grid.
+ *
+ * @since 7.5.0
+ */
+ public interface ColumnReorderListener extends Serializable {
+
+ /**
+ * Called when the columns of the grid have been reordered.
+ *
+ * @param event
+ * An event providing more information
+ */
+ void columnReorder(ColumnReorderEvent event);
+ }
+
+ /**
+ * An event that is fired when the columns are reordered.
+ *
+ * @since 7.5.0
+ */
+ public static class ColumnReorderEvent extends Component.Event {
+
+ private final boolean userOriginated;
+
+ /**
+ *
+ * @param source
+ * the grid where the event originated from
+ * @param userOriginated
+ * <code>true</code> if event is a result of user
+ * interaction, <code>false</code> if from API call
+ */
+ public ColumnReorderEvent(Grid source, boolean userOriginated) {
+ super(source);
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Returns <code>true</code> if the column reorder was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ }
+
+ /**
+ * An event listener for column resize events in the Grid.
+ *
+ * @since 7.6
+ */
+ public interface ColumnResizeListener extends Serializable {
+
+ /**
+ * Called when the columns of the grid have been resized.
+ *
+ * @param event
+ * An event providing more information
+ */
+ void columnResize(ColumnResizeEvent event);
+ }
+
+ /**
+ * An event that is fired when a column is resized, either programmatically
+ * or by the user.
+ *
+ * @since 7.6
+ */
+ public static class ColumnResizeEvent extends Component.Event {
+
+ private final Column column;
+ private final boolean userOriginated;
+
+ /**
+ *
+ * @param source
+ * the grid where the event originated from
+ * @param userOriginated
+ * <code>true</code> if event is a result of user
+ * interaction, <code>false</code> if from API call
+ */
+ public ColumnResizeEvent(Grid source, Column column,
+ boolean userOriginated) {
+ super(source);
+ this.column = column;
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Returns the column that was resized.
+ *
+ * @return the resized column.
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns <code>true</code> if the column resize was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ }
+
+ /**
+ * Interface for an editor event listener
+ */
+ public interface EditorListener extends Serializable {
+
+ public static final Method EDITOR_OPEN_METHOD = ReflectTools
+ .findMethod(EditorListener.class, "editorOpened",
+ EditorOpenEvent.class);
+ public static final Method EDITOR_MOVE_METHOD = ReflectTools
+ .findMethod(EditorListener.class, "editorMoved",
+ EditorMoveEvent.class);
+ public static final Method EDITOR_CLOSE_METHOD = ReflectTools
+ .findMethod(EditorListener.class, "editorClosed",
+ EditorCloseEvent.class);
+
+ /**
+ * Called when an editor is opened
+ *
+ * @param e
+ * an editor open event object
+ */
+ public void editorOpened(EditorOpenEvent e);
+
+ /**
+ * Called when an editor is reopened without closing it first
+ *
+ * @param e
+ * an editor move event object
+ */
+ public void editorMoved(EditorMoveEvent e);
+
+ /**
+ * Called when an editor is closed
+ *
+ * @param e
+ * an editor close event object
+ */
+ public void editorClosed(EditorCloseEvent e);
+
+ }
+
+ /**
+ * Base class for editor related events
+ */
+ public static abstract class EditorEvent extends Component.Event {
+
+ private Object itemID;
+
+ protected EditorEvent(Grid source, Object itemID) {
+ super(source);
+ this.itemID = itemID;
+ }
+
+ /**
+ * Get the item (row) for which this editor was opened
+ */
+ public Object getItem() {
+ return itemID;
+ }
+
+ }
+
+ /**
+ * This event gets fired when an editor is opened
+ */
+ public static class EditorOpenEvent extends EditorEvent {
+
+ public EditorOpenEvent(Grid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * This event gets fired when an editor is opened while another row is being
+ * edited (i.e. editor focus moves elsewhere)
+ */
+ public static class EditorMoveEvent extends EditorEvent {
+
+ public EditorMoveEvent(Grid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * This event gets fired when an editor is dismissed or closed by other
+ * means.
+ */
+ public static class EditorCloseEvent extends EditorEvent {
+
+ public EditorCloseEvent(Grid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * Default error handler for the editor
+ *
+ */
+ public class DefaultEditorErrorHandler implements EditorErrorHandler {
+
+ @Override
+ public void commitError(CommitErrorEvent event) {
+ Map<Field<?>, InvalidValueException> invalidFields = event
+ .getCause().getInvalidFields();
+
+ if (!invalidFields.isEmpty()) {
+ Object firstErrorPropertyId = null;
+ Field<?> firstErrorField = null;
+
+ FieldGroup fieldGroup = event.getCause().getFieldGroup();
+ for (Column column : getColumns()) {
+ Object propertyId = column.getPropertyId();
+ Field<?> field = fieldGroup.getField(propertyId);
+ if (invalidFields.keySet().contains(field)) {
+ event.addErrorColumn(column);
+
+ if (firstErrorPropertyId == null) {
+ firstErrorPropertyId = propertyId;
+ firstErrorField = field;
+ }
+ }
+ }
+
+ /*
+ * Validation error, show first failure as
+ * "<Column header>: <message>"
+ */
+ String caption = getColumn(firstErrorPropertyId)
+ .getHeaderCaption();
+ String message = invalidFields.get(firstErrorField)
+ .getLocalizedMessage();
+
+ event.setUserErrorMessage(caption + ": " + message);
+ } else {
+ com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this).error(
+ new ConnectorErrorEvent(Grid.this, event.getCause()));
+ }
+ }
+
+ private Object getFirstPropertyId(FieldGroup fieldGroup,
+ Set<Field<?>> keySet) {
+ for (Column c : getColumns()) {
+ Object propertyId = c.getPropertyId();
+ Field<?> f = fieldGroup.getField(propertyId);
+ if (keySet.contains(f)) {
+ return propertyId;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Selection modes representing built-in {@link SelectionModel
+ * SelectionModels} that come bundled with {@link Grid}.
+ * <p>
+ * Passing one of these enums into
+ * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling
+ * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in
+ * implementations of {@link SelectionModel}.
+ *
+ * @see Grid#setSelectionMode(SelectionMode)
+ * @see Grid#setSelectionModel(SelectionModel)
+ */
+ public enum SelectionMode {
+ /** A SelectionMode that maps to {@link SingleSelectionModel} */
+ SINGLE {
+ @Override
+ protected SelectionModel createModel() {
+ return new SingleSelectionModel();
+ }
+
+ },
+
+ /** A SelectionMode that maps to {@link MultiSelectionModel} */
+ MULTI {
+ @Override
+ protected SelectionModel createModel() {
+ return new MultiSelectionModel();
+ }
+ },
+
+ /** A SelectionMode that maps to {@link NoSelectionModel} */
+ NONE {
+ @Override
+ protected SelectionModel createModel() {
+ return new NoSelectionModel();
+ }
+ };
+
+ protected abstract SelectionModel createModel();
+ }
+
+ /**
+ * The server-side interface that controls Grid's selection state.
+ * SelectionModel should extend {@link AbstractGridExtension}.
+ */
+ public interface SelectionModel extends Serializable, Extension {
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ boolean isSelected(Object itemId);
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ Collection<Object> getSelectedRows();
+
+ /**
+ * Injects the current {@link Grid} instance into the SelectionModel.
+ * This method should usually call the extend method of
+ * {@link AbstractExtension}.
+ * <p>
+ * <em>Note:</em> This method should not be called manually.
+ *
+ * @param grid
+ * the Grid in which the SelectionModel currently is, or
+ * <code>null</code> when a selection model is being detached
+ * from a Grid.
+ */
+ void setGrid(Grid grid);
+
+ /**
+ * Resets the SelectiomModel to an initial state.
+ * <p>
+ * Most often this means that the selection state is cleared, but
+ * implementations are free to interpret the "initial state" as they
+ * wish. Some, for example, may want to keep the first selected item as
+ * selected.
+ */
+ void reset();
+
+ /**
+ * A SelectionModel that supports multiple selections to be made.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if
+ * something is forbidden to do in e.g. the user interface, it must also
+ * be forbidden to do in the server-side and client-side APIs.
+ */
+ public interface Multi extends SelectionModel {
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only
+ * adds to it.
+ *
+ * @param itemIds
+ * the itemId(s) to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code> or given itemIds don't exist in the
+ * container of Grid
+ * @see #deselect(Object...)
+ */
+ boolean select(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only
+ * adds to it.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code> or given
+ * itemIds don't exist in the container of Grid
+ * @see #deselect(Collection)
+ */
+ boolean select(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were
+ * selected previously
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code>
+ * @see #select(Object...)
+ */
+ boolean deselect(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were
+ * selected previously
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code>
+ * @see #select(Collection)
+ */
+ boolean deselect(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks all the items in the current Container as selected
+ *
+ * @return <code>true</code> iff some items were previously not
+ * selected
+ * @see #deselectAll()
+ */
+ boolean selectAll();
+
+ /**
+ * Marks all the items in the current Container as deselected
+ *
+ * @return <code>true</code> iff some items were previously selected
+ * @see #selectAll()
+ */
+ boolean deselectAll();
+
+ /**
+ * Marks items as selected while deselecting all items not in the
+ * given Collection.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code> or given
+ * itemIds don't exist in the container of Grid
+ */
+ boolean setSelected(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected while deselecting all items not in the
+ * varargs array.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code> or given itemIds don't exist in the
+ * container of Grid
+ */
+ boolean setSelected(Object... itemIds)
+ throws IllegalArgumentException;
+ }
+
+ /**
+ * A SelectionModel that supports for only single rows to be selected at
+ * a time.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if
+ * something is forbidden to do in e.g. the user interface, it must also
+ * be forbidden to do in the server-side and client-side APIs.
+ */
+ public interface Single extends SelectionModel {
+
+ /**
+ * Marks an item as selected.
+ *
+ * @param itemId
+ * the itemId to mark as selected; <code>null</code> for
+ * deselect
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might
+ * be that the given id was null, indicating a deselect,
+ * but implementation doesn't allow deselecting.
+ * re-selecting something
+ * @throws IllegalArgumentException
+ * if given itemId does not exist in the container of
+ * Grid
+ */
+ boolean select(Object itemId) throws IllegalStateException,
+ IllegalArgumentException;
+
+ /**
+ * Gets the item id of the currently selected item.
+ *
+ * @return the item id of the currently selected item, or
+ * <code>null</code> if nothing is selected
+ */
+ Object getSelectedRow();
+
+ /**
+ * Sets whether it's allowed to deselect the selected row through
+ * the UI. Deselection is allowed by default.
+ *
+ * @param deselectAllowed
+ * <code>true</code> if the selected row can be
+ * deselected without selecting another row instead;
+ * otherwise <code>false</code>.
+ */
+ public void setDeselectAllowed(boolean deselectAllowed);
+
+ /**
+ * Sets whether it's allowed to deselect the selected row through
+ * the UI.
+ *
+ * @return <code>true</code> if deselection is allowed; otherwise
+ * <code>false</code>
+ */
+ public boolean isDeselectAllowed();
+ }
+
+ /**
+ * A SelectionModel that does not allow for rows to be selected.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if the
+ * developer is unable to select something programmatically, it is not
+ * allowed for the end-user to select anything, either.
+ */
+ public interface None extends SelectionModel {
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always <code>false</code>.
+ */
+ @Override
+ public boolean isSelected(Object itemId);
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always an empty collection.
+ */
+ @Override
+ public Collection<Object> getSelectedRows();
+ }
+ }
+
+ /**
+ * A base class for SelectionModels that contains some of the logic that is
+ * reusable.
+ */
+ public static abstract class AbstractSelectionModel extends
+ AbstractGridExtension implements SelectionModel, DataGenerator {
+ protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>();
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return selection.contains(itemId);
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return new ArrayList<Object>(selection);
+ }
+
+ @Override
+ public void setGrid(final Grid grid) {
+ if (grid != null) {
+ extend(grid);
+ }
+ }
+
+ /**
+ * Sanity check for existence of item id.
+ *
+ * @param itemId
+ * item id to be selected / deselected
+ *
+ * @throws IllegalArgumentException
+ * if item Id doesn't exist in the container of Grid
+ */
+ protected void checkItemIdExists(Object itemId)
+ throws IllegalArgumentException {
+ if (!getParentGrid().getContainerDataSource().containsId(itemId)) {
+ throw new IllegalArgumentException("Given item id (" + itemId
+ + ") does not exist in the container");
+ }
+ }
+
+ /**
+ * Sanity check for existence of item ids in given collection.
+ *
+ * @param itemIds
+ * item id collection to be selected / deselected
+ *
+ * @throws IllegalArgumentException
+ * if at least one item id doesn't exist in the container of
+ * Grid
+ */
+ protected void checkItemIdsExist(Collection<?> itemIds)
+ throws IllegalArgumentException {
+ for (Object itemId : itemIds) {
+ checkItemIdExists(itemId);
+ }
+ }
+
+ /**
+ * Fires a {@link SelectionEvent} to all the {@link SelectionListener
+ * SelectionListeners} currently added to the Grid in which this
+ * SelectionModel is.
+ * <p>
+ * Note that this is only a helper method, and routes the call all the
+ * way to Grid. A {@link SelectionModel} is not a
+ * {@link SelectionNotifier}
+ *
+ * @param oldSelection
+ * the complete {@link Collection} of the itemIds that were
+ * selected <em>before</em> this event happened
+ * @param newSelection
+ * the complete {@link Collection} of the itemIds that are
+ * selected <em>after</em> this event happened
+ */
+ protected void fireSelectionEvent(
+ final Collection<Object> oldSelection,
+ final Collection<Object> newSelection) {
+ getParentGrid().fireSelectionEvent(oldSelection, newSelection);
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ if (isSelected(itemId)) {
+ rowData.put(GridState.JSONKEY_SELECTED, true);
+ }
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ // NO-OP
+ }
+
+ @Override
+ protected Object getItemId(String rowKey) {
+ return rowKey != null ? super.getItemId(rowKey) : null;
+ }
+ }
+
+ /**
+ * A default implementation of a {@link SelectionModel.Single}
+ */
+ public static class SingleSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.Single {
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ registerRpc(new SingleSelectionModelServerRpc() {
+
+ @Override
+ public void select(String rowKey) {
+ SingleSelectionModel.this.select(getItemId(rowKey), false);
+ }
+ });
+ }
+
+ @Override
+ public boolean select(final Object itemId) {
+ return select(itemId, true);
+ }
+
+ protected boolean select(final Object itemId, boolean refresh) {
+ if (itemId == null) {
+ return deselect(getSelectedRow());
+ }
+
+ checkItemIdExists(itemId);
+
+ final Object selectedRow = getSelectedRow();
+ final boolean modified = selection.add(itemId);
+ if (modified) {
+ final Collection<Object> deselected;
+ if (selectedRow != null) {
+ deselectInternal(selectedRow, false, true);
+ deselected = Collections.singleton(selectedRow);
+ } else {
+ deselected = Collections.emptySet();
+ }
+
+ fireSelectionEvent(deselected, selection);
+ }
+
+ if (refresh) {
+ refreshRow(itemId);
+ }
+
+ return modified;
+ }
+
+ private boolean deselect(final Object itemId) {
+ return deselectInternal(itemId, true, true);
+ }
+
+ private boolean deselectInternal(final Object itemId,
+ boolean fireEventIfNeeded, boolean refresh) {
+ final boolean modified = selection.remove(itemId);
+ if (modified) {
+ if (refresh) {
+ refreshRow(itemId);
+ }
+ if (fireEventIfNeeded) {
+ fireSelectionEvent(Collections.singleton(itemId),
+ Collections.emptySet());
+ }
+ }
+ return modified;
+ }
+
+ @Override
+ public Object getSelectedRow() {
+ if (selection.isEmpty()) {
+ return null;
+ } else {
+ return selection.iterator().next();
+ }
+ }
+
+ /**
+ * Resets the selection state.
+ * <p>
+ * If an item is selected, it will become deselected.
+ */
+ @Override
+ public void reset() {
+ deselect(getSelectedRow());
+ }
+
+ @Override
+ public void setDeselectAllowed(boolean deselectAllowed) {
+ getState().deselectAllowed = deselectAllowed;
+ }
+
+ @Override
+ public boolean isDeselectAllowed() {
+ return getState().deselectAllowed;
+ }
+
+ @Override
+ protected SingleSelectionModelState getState() {
+ return (SingleSelectionModelState) super.getState();
+ }
+ }
+
+ /**
+ * A default implementation for a {@link SelectionModel.None}
+ */
+ public static class NoSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.None {
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return false;
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Semantically resets the selection model.
+ * <p>
+ * Effectively a no-op.
+ */
+ @Override
+ public void reset() {
+ // NOOP
+ }
+ }
+
+ /**
+ * A default implementation of a {@link SelectionModel.Multi}
+ */
+ public static class MultiSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.Multi {
+
+ /**
+ * The default selection size limit.
+ *
+ * @see #setSelectionLimit(int)
+ */
+ public static final int DEFAULT_MAX_SELECTIONS = 1000;
+
+ private int selectionLimit = DEFAULT_MAX_SELECTIONS;
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ registerRpc(new MultiSelectionModelServerRpc() {
+
+ @Override
+ public void select(List<String> rowKeys) {
+ List<Object> items = new ArrayList<Object>();
+ for (String rowKey : rowKeys) {
+ items.add(getItemId(rowKey));
+ }
+ MultiSelectionModel.this.select(items, false);
+ }
+
+ @Override
+ public void deselect(List<String> rowKeys) {
+ List<Object> items = new ArrayList<Object>();
+ for (String rowKey : rowKeys) {
+ items.add(getItemId(rowKey));
+ }
+ MultiSelectionModel.this.deselect(items, false);
+ }
+
+ @Override
+ public void selectAll() {
+ MultiSelectionModel.this.selectAll(false);
+ }
+
+ @Override
+ public void deselectAll() {
+ MultiSelectionModel.this.deselectAll(false);
+ }
+ });
+ }
+
+ @Override
+ public boolean select(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // select will fire the event
+ return select(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * All items might not be selected if the limit set using
+ * {@link #setSelectionLimit(int)} is exceeded.
+ */
+ @Override
+ public boolean select(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ return select(itemIds, true);
+ }
+
+ protected boolean select(final Collection<?> itemIds, boolean refresh) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ // Sanity check
+ checkItemIdsExist(itemIds);
+
+ final boolean selectionWillChange = !selection.containsAll(itemIds)
+ && selection.size() < selectionLimit;
+ if (selectionWillChange) {
+ final HashSet<Object> oldSelection = new HashSet<Object>(
+ selection);
+ if (selection.size() + itemIds.size() >= selectionLimit) {
+ // Add one at a time if there's a risk of overflow
+ Iterator<?> iterator = itemIds.iterator();
+ while (iterator.hasNext()
+ && selection.size() < selectionLimit) {
+ selection.add(iterator.next());
+ }
+ } else {
+ selection.addAll(itemIds);
+ }
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ if (refresh) {
+ for (Object itemId : itemIds) {
+ refreshRow(itemId);
+ }
+ }
+
+ return selectionWillChange;
+ }
+
+ /**
+ * Sets the maximum number of rows that can be selected at once. This is
+ * a mechanism to prevent exhausting server memory in situations where
+ * users select lots of rows. If the limit is reached, newly selected
+ * rows will not become recorded.
+ * <p>
+ * Old selections are not discarded if the current number of selected
+ * row exceeds the new limit.
+ * <p>
+ * The default limit is {@value #DEFAULT_MAX_SELECTIONS} rows.
+ *
+ * @param selectionLimit
+ * the non-negative selection limit to set
+ * @throws IllegalArgumentException
+ * if the limit is negative
+ */
+ public void setSelectionLimit(int selectionLimit) {
+ if (selectionLimit < 0) {
+ throw new IllegalArgumentException(
+ "The selection limit must be non-negative");
+ }
+ this.selectionLimit = selectionLimit;
+ }
+
+ /**
+ * Gets the selection limit.
+ *
+ * @see #setSelectionLimit(int)
+ *
+ * @return the selection limit
+ */
+ public int getSelectionLimit() {
+ return selectionLimit;
+ }
+
+ @Override
+ public boolean deselect(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // deselect will fire the event
+ return deselect(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ @Override
+ public boolean deselect(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ return deselect(itemIds, true);
+ }
+
+ protected boolean deselect(final Collection<?> itemIds, boolean refresh) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ final boolean hasCommonElements = !Collections.disjoint(itemIds,
+ selection);
+ if (hasCommonElements) {
+ final HashSet<Object> oldSelection = new HashSet<Object>(
+ selection);
+ selection.removeAll(itemIds);
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ if (refresh) {
+ for (Object itemId : itemIds) {
+ refreshRow(itemId);
+ }
+ }
+
+ return hasCommonElements;
+ }
+
+ @Override
+ public boolean selectAll() {
+ return selectAll(true);
+ }
+
+ protected boolean selectAll(boolean refresh) {
+ // select will fire the event
+ final Indexed container = getParentGrid().getContainerDataSource();
+ if (container != null) {
+ return select(container.getItemIds(), refresh);
+ } else if (selection.isEmpty()) {
+ return false;
+ } else {
+ /*
+ * this should never happen (no container but has a selection),
+ * but I guess the only theoretically correct course of
+ * action...
+ */
+ return deselectAll(false);
+ }
+ }
+
+ @Override
+ public boolean deselectAll() {
+ return deselectAll(true);
+ }
+
+ protected boolean deselectAll(boolean refresh) {
+ // deselect will fire the event
+ return deselect(getSelectedRows(), refresh);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The returned Collection is in <strong>order of selection</strong>
+ * – the item that was first selected will be first in the
+ * collection, and so on. Should an item have been selected twice
+ * without being deselected in between, it will have remained in its
+ * original position.
+ */
+ @Override
+ public Collection<Object> getSelectedRows() {
+ // overridden only for JavaDoc
+ return super.getSelectedRows();
+ }
+
+ /**
+ * Resets the selection model.
+ * <p>
+ * Equivalent to calling {@link #deselectAll()}
+ */
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+
+ @Override
+ public boolean setSelected(Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ checkItemIdsExist(itemIds);
+
+ boolean changed = false;
+ Set<Object> selectedRows = new HashSet<Object>(itemIds);
+ final Collection<Object> oldSelection = getSelectedRows();
+ Set<Object> added = getDifference(selectedRows, selection);
+ if (!added.isEmpty()) {
+ changed = true;
+ selection.addAll(added);
+ for (Object id : added) {
+ refreshRow(id);
+ }
+ }
+
+ Set<Object> removed = getDifference(selection, selectedRows);
+ if (!removed.isEmpty()) {
+ changed = true;
+ selection.removeAll(removed);
+ for (Object id : removed) {
+ refreshRow(id);
+ }
+ }
+
+ if (changed) {
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ return changed;
+ }
+
+ /**
+ * Compares two sets and returns a set containing all values that are
+ * present in the first, but not in the second.
+ *
+ * @param set1
+ * first item set
+ * @param set2
+ * second item set
+ * @return all values from set1 which are not present in set2
+ */
+ private static Set<Object> getDifference(Set<Object> set1,
+ Set<Object> set2) {
+ Set<Object> diff = new HashSet<Object>(set1);
+ diff.removeAll(set2);
+ return diff;
+ }
+
+ @Override
+ public boolean setSelected(Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ return setSelected(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ private void updateAllSelectedState() {
+ int totalRowCount = getParentGrid().datasource.size();
+ int rows = Math.min(totalRowCount, selectionLimit);
+ if (getState().allSelected != selection.size() >= rows) {
+ getState().allSelected = selection.size() >= rows;
+ }
+ }
+
+ @Override
+ protected MultiSelectionModelState getState() {
+ return (MultiSelectionModelState) super.getState();
+ }
+ }
+
+ /**
+ * A data class which contains information which identifies a row in a
+ * {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance
+ * of this object is subject to change without the user knowing it and so
+ * should not be stored anywhere outside of the method providing these
+ * instances.
+ */
+ public static class RowReference implements Serializable {
+ private final Grid grid;
+
+ private Object itemId;
+
+ /**
+ * Creates a new row reference for the given grid.
+ *
+ * @param grid
+ * the grid that the row belongs to
+ */
+ public RowReference(Grid grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the identifying information for this row
+ *
+ * @param itemId
+ * the item id of the row
+ */
+ public void set(Object itemId) {
+ this.itemId = itemId;
+ }
+
+ /**
+ * Gets the grid that contains the referenced row.
+ *
+ * @return the grid that contains referenced row
+ */
+ public Grid getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the item id of the row.
+ *
+ * @return the item id of the row
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Gets the item for the row.
+ *
+ * @return the item for the row
+ */
+ public Item getItem() {
+ return grid.getContainerDataSource().getItem(itemId);
+ }
+ }
+
+ /**
+ * A data class which contains information which identifies a cell in a
+ * {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance
+ * of this object is subject to change without the user knowing it and so
+ * should not be stored anywhere outside of the method providing these
+ * instances.
+ */
+ public static class CellReference implements Serializable {
+ private final RowReference rowReference;
+
+ private Object propertyId;
+
+ public CellReference(RowReference rowReference) {
+ this.rowReference = rowReference;
+ }
+
+ /**
+ * Sets the identifying information for this cell
+ *
+ * @param propertyId
+ * the property id of the column
+ */
+ public void set(Object propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ /**
+ * Gets the grid that contains the referenced cell.
+ *
+ * @return the grid that contains referenced cell
+ */
+ public Grid getGrid() {
+ return rowReference.getGrid();
+ }
+
+ /**
+ * @return the property id of the column
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * @return the property for the cell
+ */
+ public Property<?> getProperty() {
+ return getItem().getItemProperty(propertyId);
+ }
+
+ /**
+ * Gets the item id of the row of the cell.
+ *
+ * @return the item id of the row
+ */
+ public Object getItemId() {
+ return rowReference.getItemId();
+ }
+
+ /**
+ * Gets the item for the row of the cell.
+ *
+ * @return the item for the row
+ */
+ public Item getItem() {
+ return rowReference.getItem();
+ }
+
+ /**
+ * Gets the value of the cell.
+ *
+ * @return the value of the cell
+ */
+ public Object getValue() {
+ return getProperty().getValue();
+ }
+ }
+
+ /**
+ * A callback interface for generating custom style names for Grid rows.
+ *
+ * @see Grid#setRowStyleGenerator(RowStyleGenerator)
+ */
+ public interface RowStyleGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a row.
+ *
+ * @param row
+ * the row to generate a style for
+ * @return the style name to add to this row, or {@code null} to not set
+ * any style
+ */
+ public String getStyle(RowReference row);
+ }
+
+ /**
+ * A callback interface for generating custom style names for Grid cells.
+ *
+ * @see Grid#setCellStyleGenerator(CellStyleGenerator)
+ */
+ public interface CellStyleGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a column.
+ *
+ * @param cell
+ * the cell to generate a style for
+ * @return the style name to add to this cell, or {@code null} to not
+ * set any style
+ */
+ public String getStyle(CellReference cell);
+ }
+
+ /**
+ * A callback interface for generating optional descriptions (tooltips) for
+ * Grid rows. If a description is generated for a row, it is used for all
+ * the cells in the row for which a {@link CellDescriptionGenerator cell
+ * description} is not generated.
+ *
+ * @see Grid#setRowDescriptionGenerator
+ *
+ * @since 7.6
+ */
+ public interface RowDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a description (tooltip) for a row. The
+ * description may contain HTML which is rendered directly; if this is
+ * not desired the returned string must be escaped by the implementing
+ * method.
+ *
+ * @param row
+ * the row to generate a description for
+ * @return the row description or {@code null} for no description
+ */
+ public String getDescription(RowReference row);
+ }
+
+ /**
+ * A callback interface for generating optional descriptions (tooltips) for
+ * Grid cells. If a cell has both a {@link RowDescriptionGenerator row
+ * description}Â and a cell description, the latter has precedence.
+ *
+ * @see Grid#setCellDescriptionGenerator(CellDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public interface CellDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a description (tooltip) for a cell. The
+ * description may contain HTML which is rendered directly; if this is
+ * not desired the returned string must be escaped by the implementing
+ * method.
+ *
+ * @param cell
+ * the cell to generate a description for
+ * @return the cell description or {@code null} for no description
+ */
+ public String getDescription(CellReference cell);
+ }
+
+ /**
+ * Class for generating all row and cell related data for the essential
+ * parts of Grid.
+ */
+ private class RowDataGenerator implements DataGenerator {
+
+ private void put(String key, String value, JsonObject object) {
+ if (value != null && !value.isEmpty()) {
+ object.put(key, value);
+ }
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ RowReference row = new RowReference(Grid.this);
+ row.set(itemId);
+
+ if (rowStyleGenerator != null) {
+ String style = rowStyleGenerator.getStyle(row);
+ put(GridState.JSONKEY_ROWSTYLE, style, rowData);
+ }
+
+ if (rowDescriptionGenerator != null) {
+ String description = rowDescriptionGenerator
+ .getDescription(row);
+ put(GridState.JSONKEY_ROWDESCRIPTION, description, rowData);
+
+ }
+
+ JsonObject cellStyles = Json.createObject();
+ JsonObject cellData = Json.createObject();
+ JsonObject cellDescriptions = Json.createObject();
+
+ CellReference cell = new CellReference(row);
+
+ for (Column column : getColumns()) {
+ cell.set(column.getPropertyId());
+
+ writeData(cell, cellData);
+ writeStyles(cell, cellStyles);
+ writeDescriptions(cell, cellDescriptions);
+ }
+
+ if (cellDescriptionGenerator != null
+ && cellDescriptions.keys().length > 0) {
+ rowData.put(GridState.JSONKEY_CELLDESCRIPTION, cellDescriptions);
+ }
+
+ if (cellStyleGenerator != null && cellStyles.keys().length > 0) {
+ rowData.put(GridState.JSONKEY_CELLSTYLES, cellStyles);
+ }
+
+ rowData.put(GridState.JSONKEY_DATA, cellData);
+ }
+
+ private void writeStyles(CellReference cell, JsonObject styles) {
+ if (cellStyleGenerator != null) {
+ String style = cellStyleGenerator.getStyle(cell);
+ put(columnKeys.key(cell.getPropertyId()), style, styles);
+ }
+ }
+
+ private void writeDescriptions(CellReference cell,
+ JsonObject descriptions) {
+ if (cellDescriptionGenerator != null) {
+ String description = cellDescriptionGenerator
+ .getDescription(cell);
+ put(columnKeys.key(cell.getPropertyId()), description,
+ descriptions);
+ }
+ }
+
+ private void writeData(CellReference cell, JsonObject data) {
+ Column column = getColumn(cell.getPropertyId());
+ Converter<?, ?> converter = column.getConverter();
+ Renderer<?> renderer = column.getRenderer();
+
+ Item item = cell.getItem();
+ Object modelValue = item.getItemProperty(cell.getPropertyId())
+ .getValue();
+
+ data.put(columnKeys.key(cell.getPropertyId()), AbstractRenderer
+ .encodeValue(modelValue, renderer, converter, getLocale()));
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ // NO-OP
+ }
+ }
+
+ /**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @since 7.6
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+ public abstract static class StaticSection<ROWTYPE extends StaticSection.StaticRow<?>>
+ implements Serializable {
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ public abstract static class StaticRow<CELLTYPE extends StaticCell>
+ implements Serializable {
+
+ private RowState rowState = new RowState();
+ protected StaticSection<?> section;
+ private Map<Object, CELLTYPE> cells = new LinkedHashMap<Object, CELLTYPE>();
+ private Map<Set<CELLTYPE>, CELLTYPE> cellGroups = new HashMap<Set<CELLTYPE>, CELLTYPE>();
+
+ protected StaticRow(StaticSection<?> section) {
+ this.section = section;
+ }
+
+ protected void addCell(Object propertyId) {
+ CELLTYPE cell = createCell();
+ cell.setColumnId(section.grid.getColumn(propertyId).getState().id);
+ cells.put(propertyId, cell);
+ rowState.cells.add(cell.getCellState());
+ }
+
+ protected void removeCell(Object propertyId) {
+ CELLTYPE cell = cells.remove(propertyId);
+ if (cell != null) {
+ Set<CELLTYPE> cellGroupForCell = getCellGroupForCell(cell);
+ if (cellGroupForCell != null) {
+ removeCellFromGroup(cell, cellGroupForCell);
+ }
+ rowState.cells.remove(cell.getCellState());
+ }
+ }
+
+ private void removeCellFromGroup(CELLTYPE cell,
+ Set<CELLTYPE> cellGroup) {
+ String columnId = cell.getColumnId();
+ for (Set<String> group : rowState.cellGroups.keySet()) {
+ if (group.contains(columnId)) {
+ if (group.size() > 2) {
+ // Update map key correctly
+ CELLTYPE mergedCell = cellGroups.remove(cellGroup);
+ cellGroup.remove(cell);
+ cellGroups.put(cellGroup, mergedCell);
+
+ group.remove(columnId);
+ } else {
+ rowState.cellGroups.remove(group);
+ cellGroups.remove(cellGroup);
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Creates and returns a new instance of the cell type.
+ *
+ * @return the created cell
+ */
+ protected abstract CELLTYPE createCell();
+
+ protected RowState getRowState() {
+ return rowState;
+ }
+
+ /**
+ * Returns the cell for the given property id on this row. If the
+ * column is merged returned cell is the cell for the whole group.
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the cell for the given property, merged cell for merged
+ * properties, null if not found
+ */
+ public CELLTYPE getCell(Object propertyId) {
+ CELLTYPE cell = cells.get(propertyId);
+ Set<CELLTYPE> cellGroup = getCellGroupForCell(cell);
+ if (cellGroup != null) {
+ cell = cellGroups.get(cellGroup);
+ }
+ return cell;
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param propertyIds
+ * The property ids of columns to merge
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(Object... propertyIds) {
+ assert propertyIds.length > 1 : "You need to merge at least 2 properties";
+
+ Set<CELLTYPE> cells = new HashSet<CELLTYPE>();
+ for (int i = 0; i < propertyIds.length; ++i) {
+ cells.add(getCell(propertyIds[i]));
+ }
+
+ return join(cells);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ assert cells.length > 1 : "You need to merge at least 2 cells";
+
+ return join(new HashSet<CELLTYPE>(Arrays.asList(cells)));
+ }
+
+ protected CELLTYPE join(Set<CELLTYPE> cells) {
+ for (CELLTYPE cell : cells) {
+ if (getCellGroupForCell(cell) != null) {
+ throw new IllegalArgumentException(
+ "Cell already merged");
+ } else if (!this.cells.containsValue(cell)) {
+ throw new IllegalArgumentException(
+ "Cell does not exist on this row");
+ }
+ }
+
+ // Create new cell data for the group
+ CELLTYPE newCell = createCell();
+
+ Set<String> columnGroup = new HashSet<String>();
+ for (CELLTYPE cell : cells) {
+ columnGroup.add(cell.getColumnId());
+ }
+ rowState.cellGroups.put(columnGroup, newCell.getCellState());
+ cellGroups.put(cells, newCell);
+ return newCell;
+ }
+
+ private Set<CELLTYPE> getCellGroupForCell(CELLTYPE cell) {
+ for (Set<CELLTYPE> group : cellGroups.keySet()) {
+ if (group.contains(cell)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the custom style name for this row.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return getRowState().styleName;
+ }
+
+ /**
+ * Sets a custom style name for this row.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ getRowState().styleName = styleName;
+ }
+
+ /**
+ * Writes the declarative design to the given table row element.
+ *
+ * @since 7.5.0
+ * @param trElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element trElement,
+ DesignContext designContext) {
+ Set<CELLTYPE> visited = new HashSet<CELLTYPE>();
+ for (Grid.Column column : section.grid.getColumns()) {
+ CELLTYPE cell = getCell(column.getPropertyId());
+ if (visited.contains(cell)) {
+ continue;
+ }
+ visited.add(cell);
+
+ Element cellElement = trElement
+ .appendElement(getCellTagName());
+ cell.writeDesign(cellElement, designContext);
+
+ for (Entry<Set<CELLTYPE>, CELLTYPE> entry : cellGroups
+ .entrySet()) {
+ if (entry.getValue() == cell) {
+ cellElement.attr("colspan", ""
+ + entry.getKey().size());
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads the declarative design from the given table row element.
+ *
+ * @since 7.5.0
+ * @param trElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ * @throws DesignException
+ * if the given table row contains unexpected children
+ */
+ protected void readDesign(Element trElement,
+ DesignContext designContext) throws DesignException {
+ Elements cellElements = trElement.children();
+ int totalColSpans = 0;
+ for (int i = 0; i < cellElements.size(); ++i) {
+ Element element = cellElements.get(i);
+ if (!element.tagName().equals(getCellTagName())) {
+ throw new DesignException(
+ "Unexpected element in tr while expecting "
+ + getCellTagName() + ": "
+ + element.tagName());
+ }
+
+ int columnIndex = i + totalColSpans;
+
+ int colspan = DesignAttributeHandler.readAttribute(
+ "colspan", element.attributes(), 1, int.class);
+
+ Set<CELLTYPE> cells = new HashSet<CELLTYPE>();
+ for (int c = 0; c < colspan; ++c) {
+ cells.add(getCell(section.grid.getColumns()
+ .get(columnIndex + c).getPropertyId()));
+ }
+
+ if (colspan > 1) {
+ totalColSpans += colspan - 1;
+ join(cells).readDesign(element, designContext);
+ } else {
+ cells.iterator().next()
+ .readDesign(element, designContext);
+ }
+ }
+ }
+
+ abstract protected String getCellTagName();
+
+ void detach() {
+ for (CELLTYPE cell : cells.values()) {
+ cell.detach();
+ }
+ }
+ }
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ */
+ abstract static class StaticCell implements Serializable {
+
+ private CellState cellState = new CellState();
+ private StaticRow<?> row;
+
+ protected StaticCell(StaticRow<?> row) {
+ this.row = row;
+ }
+
+ void setColumnId(String id) {
+ cellState.columnId = id;
+ }
+
+ String getColumnId() {
+ return cellState.columnId;
+ }
+
+ /**
+ * Gets the row where this cell is.
+ *
+ * @return row for this cell
+ */
+ public StaticRow<?> getRow() {
+ return row;
+ }
+
+ protected CellState getCellState() {
+ return cellState;
+ }
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ removeComponentIfPresent();
+ cellState.text = text;
+ cellState.type = GridStaticCellType.TEXT;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (cellState.type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type "
+ + cellState.type);
+ }
+ return cellState.text;
+ }
+
+ /**
+ * Returns the HTML content displayed in this cell.
+ *
+ * @return the html
+ *
+ */
+ public String getHtml() {
+ if (cellState.type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type "
+ + cellState.type);
+ }
+ return cellState.html;
+ }
+
+ /**
+ * Sets the HTML content displayed in this cell.
+ *
+ * @param html
+ * the html to set
+ */
+ public void setHtml(String html) {
+ removeComponentIfPresent();
+ cellState.html = html;
+ cellState.type = GridStaticCellType.HTML;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the component displayed in this cell.
+ *
+ * @return the component
+ */
+ public Component getComponent() {
+ if (cellState.type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Component from a cell with type "
+ + cellState.type);
+ }
+ return (Component) cellState.connector;
+ }
+
+ /**
+ * Sets the component displayed in this cell.
+ *
+ * @param component
+ * the component to set
+ */
+ public void setComponent(Component component) {
+ removeComponentIfPresent();
+ component.setParent(row.section.grid);
+ cellState.connector = component;
+ cellState.type = GridStaticCellType.WIDGET;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the type of content stored in this cell.
+ *
+ * @return cell content type
+ */
+ public GridStaticCellType getCellType() {
+ return cellState.type;
+ }
+
+ /**
+ * Returns the custom style name for this cell.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return cellState.styleName;
+ }
+
+ /**
+ * Sets a custom style name for this cell.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ cellState.styleName = styleName;
+ row.section.markAsDirty();
+ }
+
+ private void removeComponentIfPresent() {
+ Component component = (Component) cellState.connector;
+ if (component != null) {
+ component.setParent(null);
+ cellState.connector = null;
+ }
+ }
+
+ /**
+ * Writes the declarative design to the given table cell element.
+ *
+ * @since 7.5.0
+ * @param cellElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element cellElement,
+ DesignContext designContext) {
+ switch (cellState.type) {
+ case TEXT:
+ cellElement.attr("plain-text", true);
+ cellElement.appendText(getText());
+ break;
+ case HTML:
+ cellElement.append(getHtml());
+ break;
+ case WIDGET:
+ cellElement.appendChild(designContext
+ .createElement(getComponent()));
+ break;
+ }
+ }
+
+ /**
+ * Reads the declarative design from the given table cell element.
+ *
+ * @since 7.5.0
+ * @param cellElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ */
+ protected void readDesign(Element cellElement,
+ DesignContext designContext) {
+ if (!cellElement.hasAttr("plain-text")) {
+ if (cellElement.children().size() > 0
+ && cellElement.child(0).tagName().contains("-")) {
+ setComponent(designContext.readDesign(cellElement
+ .child(0)));
+ } else {
+ setHtml(cellElement.html());
+ }
+ } else {
+ // text – need to unescape HTML entities
+ setText(DesignFormatter.decodeFromTextNode(cellElement
+ .html()));
+ }
+ }
+
+ void detach() {
+ removeComponentIfPresent();
+ }
+ }
+
+ protected Grid grid;
+ protected List<ROWTYPE> rows = new ArrayList<ROWTYPE>();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ if (getSectionState().visible != visible) {
+ getSectionState().visible = visible;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return getSectionState().visible;
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeRow(StaticRow)
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ */
+ public ROWTYPE removeRow(int rowIndex) {
+ if (rowIndex >= rows.size() || rowIndex < 0) {
+ throw new IllegalArgumentException("No row at given index "
+ + rowIndex);
+ }
+ ROWTYPE row = rows.remove(rowIndex);
+ row.detach();
+ getSectionState().rows.remove(rowIndex);
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeRow(int)
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Gets row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return row at given index
+ */
+ public ROWTYPE getRow(int rowIndex) {
+ if (rowIndex >= rows.size() || rowIndex < 0) {
+ throw new IllegalArgumentException("No row at given index "
+ + rowIndex);
+ }
+ return rows.get(rowIndex);
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ * @see #appendRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE prependRow() {
+ return addRowAt(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ * @see #prependRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE appendRow() {
+ return addRowAt(rows.size());
+ }
+
+ /**
+ * Inserts a new row at the given position.
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE addRowAt(int index) {
+ if (index > rows.size() || index < 0) {
+ throw new IllegalArgumentException(
+ "Unable to add row at index " + index);
+ }
+ ROWTYPE row = createRow();
+ rows.add(index, row);
+ getSectionState().rows.add(index, row.getRowState());
+
+ for (Object id : grid.columns.keySet()) {
+ row.addCell(id);
+ }
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Gets the amount of rows in this section.
+ *
+ * @return row count
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected abstract GridStaticSectionState getSectionState();
+
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that state has changed and it should be redrawn.
+ */
+ protected void markAsDirty() {
+ grid.markAsDirty();
+ }
+
+ /**
+ * Removes a column for given property id from the section.
+ *
+ * @param propertyId
+ * property to be removed
+ */
+ protected void removeColumn(Object propertyId) {
+ for (ROWTYPE row : rows) {
+ row.removeCell(propertyId);
+ }
+ }
+
+ /**
+ * Adds a column for given property id to the section.
+ *
+ * @param propertyId
+ * property to be added
+ */
+ protected void addColumn(Object propertyId) {
+ for (ROWTYPE row : rows) {
+ row.addCell(propertyId);
+ }
+ }
+
+ /**
+ * Performs a sanity check that section is in correct state.
+ *
+ * @throws IllegalStateException
+ * if merged cells are not i n continuous range
+ */
+ protected void sanityCheck() throws IllegalStateException {
+ List<String> columnOrder = grid.getState().columnOrder;
+ for (ROWTYPE row : rows) {
+ for (Set<String> cellGroup : row.getRowState().cellGroups
+ .keySet()) {
+ if (!checkCellGroupAndOrder(columnOrder, cellGroup)) {
+ throw new IllegalStateException(
+ "Not all merged cells were in a continuous range.");
+ }
+ }
+ }
+ }
+
+ private boolean checkCellGroupAndOrder(List<String> columnOrder,
+ Set<String> cellGroup) {
+ if (!columnOrder.containsAll(cellGroup)) {
+ return false;
+ }
+
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ if (!cellGroup.contains(columnOrder.get(i))) {
+ continue;
+ }
+
+ for (int j = 1; j < cellGroup.size(); ++j) {
+ if (!cellGroup.contains(columnOrder.get(i + j))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Writes the declarative design to the given table section element.
+ *
+ * @since 7.5.0
+ * @param tableSectionElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element tableSectionElement,
+ DesignContext designContext) {
+ for (ROWTYPE row : rows) {
+ row.writeDesign(tableSectionElement.appendElement("tr"),
+ designContext);
+ }
+ }
+
+ /**
+ * Writes the declarative design from the given table section element.
+ *
+ * @since 7.5.0
+ * @param tableSectionElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ * @throws DesignException
+ * if the table section contains unexpected children
+ */
+ protected void readDesign(Element tableSectionElement,
+ DesignContext designContext) throws DesignException {
+ while (rows.size() > 0) {
+ removeRow(0);
+ }
+
+ for (Element row : tableSectionElement.children()) {
+ if (!row.tagName().equals("tr")) {
+ throw new DesignException("Unexpected element in "
+ + tableSectionElement.tagName() + ": "
+ + row.tagName());
+ }
+ appendRow().readDesign(row, designContext);
+ }
+ }
+ }
+
+ /**
+ * Represents the header section of a Grid.
+ */
+ protected static class Header extends StaticSection<HeaderRow> {
+
+ private HeaderRow defaultRow = null;
+ private final GridStaticSectionState headerState = new GridStaticSectionState();
+
+ protected Header(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).header = headerState;
+ HeaderRow row = createRow();
+ rows.add(row);
+ setDefaultRow(row);
+ getSectionState().rows.add(row.getRowState());
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special
+ * header row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+
+ if (row != null && !rows.contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the section");
+ }
+
+ if (defaultRow != null) {
+ defaultRow.setDefaultRow(false);
+ }
+
+ if (row != null) {
+ row.setDefaultRow(true);
+ }
+
+ defaultRow = row;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return headerState;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow(this);
+ }
+
+ @Override
+ public HeaderRow removeRow(int rowIndex) {
+ HeaderRow row = super.removeRow(rowIndex);
+ if (row == defaultRow) {
+ // Default Header Row was just removed.
+ setDefaultRow(null);
+ }
+ return row;
+ }
+
+ @Override
+ protected void sanityCheck() throws IllegalStateException {
+ super.sanityCheck();
+
+ boolean hasDefaultRow = false;
+ for (HeaderRow row : rows) {
+ if (row.getRowState().defaultRow) {
+ if (!hasDefaultRow) {
+ hasDefaultRow = true;
+ } else {
+ throw new IllegalStateException(
+ "Multiple default rows in header");
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void readDesign(Element tableSectionElement,
+ DesignContext designContext) {
+ super.readDesign(tableSectionElement, designContext);
+
+ if (defaultRow == null && !rows.isEmpty()) {
+ grid.setDefaultHeaderRow(rows.get(0));
+ }
+ }
+ }
+
+ /**
+ * Represents a header row in Grid.
+ */
+ public static class HeaderRow extends StaticSection.StaticRow<HeaderCell> {
+
+ protected HeaderRow(StaticSection<?> section) {
+ super(section);
+ }
+
+ private void setDefaultRow(boolean value) {
+ getRowState().defaultRow = value;
+ }
+
+ private boolean isDefaultRow() {
+ return getRowState().defaultRow;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell(this);
+ }
+
+ @Override
+ protected String getCellTagName() {
+ return "th";
+ }
+
+ @Override
+ protected void writeDesign(Element trElement,
+ DesignContext designContext) {
+ super.writeDesign(trElement, designContext);
+
+ if (section.grid.getDefaultHeaderRow() == this) {
+ DesignAttributeHandler.writeAttribute("default",
+ trElement.attributes(), true, null, boolean.class);
+ }
+ }
+
+ @Override
+ protected void readDesign(Element trElement, DesignContext designContext) {
+ super.readDesign(trElement, designContext);
+
+ boolean defaultRow = DesignAttributeHandler.readAttribute(
+ "default", trElement.attributes(), false, boolean.class);
+ if (defaultRow) {
+ section.grid.setDefaultHeaderRow(this);
+ }
+ }
+ }
+
+ /**
+ * Represents a header cell in Grid. Can be a merged cell for multiple
+ * columns.
+ */
+ public static class HeaderCell extends StaticSection.StaticCell {
+
+ protected HeaderCell(HeaderRow row) {
+ super(row);
+ }
+ }
+
+ /**
+ * Represents the footer section of a Grid. By default Footer is not
+ * visible.
+ */
+ protected static class Footer extends StaticSection<FooterRow> {
+
+ private final GridStaticSectionState footerState = new GridStaticSectionState();
+
+ protected Footer(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).footer = footerState;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return footerState;
+ }
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow(this);
+ }
+
+ @Override
+ protected void sanityCheck() throws IllegalStateException {
+ super.sanityCheck();
+ }
+ }
+
+ /**
+ * Represents a footer row in Grid.
+ */
+ public static class FooterRow extends StaticSection.StaticRow<FooterCell> {
+
+ protected FooterRow(StaticSection<?> section) {
+ super(section);
+ }
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell(this);
+ }
+
+ @Override
+ protected String getCellTagName() {
+ return "td";
+ }
+
+ }
+
+ /**
+ * Represents a footer cell in Grid.
+ */
+ public static class FooterCell extends StaticSection.StaticCell {
+
+ protected FooterCell(FooterRow row) {
+ super(row);
+ }
+ }
+
+ /**
+ * A column in the grid. Can be obtained by calling
+ * {@link Grid#getColumn(Object propertyId)}.
+ */
+ public static class Column implements Serializable {
+
+ /**
+ * The state of the column shared to the client
+ */
+ private final GridColumnState state;
+
+ /**
+ * The grid this column is associated with
+ */
+ private final Grid grid;
+
+ /**
+ * Backing property for column
+ */
+ private final Object propertyId;
+
+ private Converter<?, Object> converter;
+
+ /**
+ * A check for allowing the
+ * {@link #Column(Grid, GridColumnState, Object) constructor} to call
+ * {@link #setConverter(Converter)} with a <code>null</code>, even if
+ * model and renderer aren't compatible.
+ */
+ private boolean isFirstConverterAssignment = true;
+
+ /**
+ * Internally used constructor.
+ *
+ * @param grid
+ * The grid this column belongs to. Should not be null.
+ * @param state
+ * the shared state of this column
+ * @param propertyId
+ * the backing property id for this column
+ */
+ Column(Grid grid, GridColumnState state, Object propertyId) {
+ this.grid = grid;
+ this.state = state;
+ this.propertyId = propertyId;
+ internalSetRenderer(new TextRenderer());
+ }
+
+ /**
+ * Returns the serializable state of this column that is sent to the
+ * client side connector.
+ *
+ * @return the internal state of the column
+ */
+ GridColumnState getState() {
+ return state;
+ }
+
+ /**
+ * Returns the property id for the backing property of this Column
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the caption of the header. By default the header caption is
+ * the property id of the column.
+ *
+ * @return the text in the default row of header.
+ *
+ * @throws IllegalStateException
+ * if the column no longer is attached to the grid
+ */
+ public String getHeaderCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+
+ return state.headerCaption;
+ }
+
+ /**
+ * Sets the caption of the header. This caption is also used as the
+ * hiding toggle caption, unless it is explicitly set via
+ * {@link #setHidingToggleCaption(String)}.
+ *
+ * @param caption
+ * the text to show in the caption
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column setHeaderCaption(String caption)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+ if (caption == null) {
+ caption = ""; // Render null as empty
+ }
+ state.headerCaption = caption;
+
+ HeaderRow row = grid.getHeader().getDefaultRow();
+ if (row != null) {
+ row.getCell(grid.getPropertyIdByColumnId(state.id)).setText(
+ caption);
+ }
+ return this;
+ }
+
+ /**
+ * Gets the caption of the hiding toggle for this column.
+ *
+ * @since 7.5.0
+ * @see #setHidingToggleCaption(String)
+ * @return the caption for the hiding toggle for this column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public String getHidingToggleCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.hidingToggleCaption;
+ }
+
+ /**
+ * Sets the caption of the hiding toggle for this column. Shown in the
+ * toggle for this column in the grid's sidebar when the column is
+ * {@link #isHidable() hidable}.
+ * <p>
+ * The default value is <code>null</code>, and in that case the column's
+ * {@link #getHeaderCaption() header caption} is used.
+ * <p>
+ * <em>NOTE:</em> setting this to empty string might cause the hiding
+ * toggle to not render correctly.
+ *
+ * @since 7.5.0
+ * @param hidingToggleCaption
+ * the text to show in the column hiding toggle
+ * @return the column itself
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column setHidingToggleCaption(String hidingToggleCaption)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+ state.hidingToggleCaption = hidingToggleCaption;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the width (in pixels). By default a column is 100px wide.
+ *
+ * @return the width in pixels of the column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public double getWidth() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.width;
+ }
+
+ /**
+ * Sets the width (in pixels).
+ * <p>
+ * This overrides any configuration set by any of
+ * {@link #setExpandRatio(int)}, {@link #setMinimumWidth(double)} or
+ * {@link #setMaximumWidth(double)}.
+ *
+ * @param pixelWidth
+ * the new pixel width of the column
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @throws IllegalArgumentException
+ * thrown if pixel width is less than zero
+ */
+ public Column setWidth(double pixelWidth) throws IllegalStateException,
+ IllegalArgumentException {
+ checkColumnIsAttached();
+ if (pixelWidth < 0) {
+ throw new IllegalArgumentException(
+ "Pixel width should be greated than 0 (in "
+ + toString() + ")");
+ }
+ if (state.width != pixelWidth) {
+ state.width = pixelWidth;
+ grid.markAsDirty();
+ grid.fireColumnResizeEvent(this, false);
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column has an undefined width.
+ *
+ * @since 7.6
+ * @return whether the width is undefined
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public boolean isWidthUndefined() {
+ checkColumnIsAttached();
+ return state.width < 0;
+ }
+
+ /**
+ * Marks the column width as undefined. An undefined width means the
+ * grid is free to resize the column based on the cell contents and
+ * available space in the grid.
+ *
+ * @return the column itself
+ */
+ public Column setWidthUndefined() {
+ checkColumnIsAttached();
+ if (!isWidthUndefined()) {
+ state.width = -1;
+ grid.markAsDirty();
+ grid.fireColumnResizeEvent(this, false);
+ }
+ return this;
+ }
+
+ /**
+ * Checks if column is attached and throws an
+ * {@link IllegalStateException} if it is not
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ protected void checkColumnIsAttached() throws IllegalStateException {
+ if (grid.getColumnByColumnId(state.id) == null) {
+ throw new IllegalStateException("Column no longer exists.");
+ }
+ }
+
+ /**
+ * Sets this column as the last frozen column in its grid.
+ *
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the column is no longer attached to any grid
+ * @see Grid#setFrozenColumnCount(int)
+ */
+ public Column setLastFrozenColumn() {
+ checkColumnIsAttached();
+ grid.setFrozenColumnCount(grid.getState(false).columnOrder
+ .indexOf(getState().id) + 1);
+ return this;
+ }
+
+ /**
+ * Sets the renderer for this column.
+ * <p>
+ * If a suitable converter isn't defined explicitly, the session
+ * converter factory is used to find a compatible converter.
+ *
+ * @param renderer
+ * the renderer to use
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if no compatible converter could be found
+ *
+ * @see VaadinSession#getConverterFactory()
+ * @see ConverterUtil#getConverter(Class, Class, VaadinSession)
+ * @see #setConverter(Converter)
+ */
+ public Column setRenderer(Renderer<?> renderer) {
+ if (!internalSetRenderer(renderer)) {
+ throw new IllegalArgumentException(
+ "Could not find a converter for converting from the model type "
+ + getModelType()
+ + " to the renderer presentation type "
+ + renderer.getPresentationType() + " (in "
+ + toString() + ")");
+ }
+ return this;
+ }
+
+ /**
+ * Sets the renderer for this column and the converter used to convert
+ * from the property value type to the renderer presentation type.
+ *
+ * @param renderer
+ * the renderer to use, cannot be null
+ * @param converter
+ * the converter to use
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the renderer is already associated with a grid column
+ */
+ public <T> Column setRenderer(Renderer<T> renderer,
+ Converter<? extends T, ?> converter) {
+ if (renderer.getParent() != null) {
+ throw new IllegalArgumentException(
+ "Cannot set a renderer that is already connected to a grid column (in "
+ + toString() + ")");
+ }
+
+ if (getRenderer() != null) {
+ grid.removeExtension(getRenderer());
+ }
+
+ grid.addRenderer(renderer);
+ state.rendererConnector = renderer;
+ setConverter(converter);
+ return this;
+ }
+
+ /**
+ * Sets the converter used to convert from the property value type to
+ * the renderer presentation type.
+ *
+ * @param converter
+ * the converter to use, or {@code null} to not use any
+ * converters
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the types are not compatible
+ */
+ public Column setConverter(Converter<?, ?> converter)
+ throws IllegalArgumentException {
+ Class<?> modelType = getModelType();
+ if (converter != null) {
+ if (!converter.getModelType().isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException(
+ "The converter model type "
+ + converter.getModelType()
+ + " is not compatible with the property type "
+ + modelType + " (in " + toString() + ")");
+
+ } else if (!getRenderer().getPresentationType()
+ .isAssignableFrom(converter.getPresentationType())) {
+ throw new IllegalArgumentException(
+ "The converter presentation type "
+ + converter.getPresentationType()
+ + " is not compatible with the renderer presentation type "
+ + getRenderer().getPresentationType()
+ + " (in " + toString() + ")");
+ }
+ }
+
+ else {
+ /*
+ * Since the converter is null (i.e. will be removed), we need
+ * to know that the renderer and model are compatible. If not,
+ * we can't allow for this to happen.
+ *
+ * The constructor is allowed to call this method with null
+ * without any compatibility checks, therefore we have a special
+ * case for it.
+ */
+
+ Class<?> rendererPresentationType = getRenderer()
+ .getPresentationType();
+ if (!isFirstConverterAssignment
+ && !rendererPresentationType
+ .isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException(
+ "Cannot remove converter, "
+ + "as renderer's presentation type "
+ + rendererPresentationType.getName()
+ + " and column's "
+ + "model "
+ + modelType.getName()
+ + " type aren't "
+ + "directly compatible with each other (in "
+ + toString() + ")");
+ }
+ }
+
+ isFirstConverterAssignment = false;
+
+ @SuppressWarnings("unchecked")
+ Converter<?, Object> castConverter = (Converter<?, Object>) converter;
+ this.converter = castConverter;
+
+ return this;
+ }
+
+ /**
+ * Returns the renderer instance used by this column.
+ *
+ * @return the renderer
+ */
+ public Renderer<?> getRenderer() {
+ return (Renderer<?>) getState().rendererConnector;
+ }
+
+ /**
+ * Returns the converter instance used by this column.
+ *
+ * @return the converter
+ */
+ public Converter<?, ?> getConverter() {
+ return converter;
+ }
+
+ private <T> boolean internalSetRenderer(Renderer<T> renderer) {
+
+ Converter<? extends T, ?> converter;
+ if (isCompatibleWithProperty(renderer, getConverter())) {
+ // Use the existing converter (possibly none) if types
+ // compatible
+ converter = (Converter<? extends T, ?>) getConverter();
+ } else {
+ converter = ConverterUtil.getConverter(
+ renderer.getPresentationType(), getModelType(),
+ getSession());
+ }
+ setRenderer(renderer, converter);
+ return isCompatibleWithProperty(renderer, converter);
+ }
+
+ private VaadinSession getSession() {
+ UI ui = grid.getUI();
+ return ui != null ? ui.getSession() : null;
+ }
+
+ private boolean isCompatibleWithProperty(Renderer<?> renderer,
+ Converter<?, ?> converter) {
+ Class<?> type;
+ if (converter == null) {
+ type = getModelType();
+ } else {
+ type = converter.getPresentationType();
+ }
+ return renderer.getPresentationType().isAssignableFrom(type);
+ }
+
+ private Class<?> getModelType() {
+ return grid.getContainerDataSource().getType(
+ grid.getPropertyIdByColumnId(state.id));
+ }
+
+ /**
+ * Sets whether this column is sortable by the user. The grid can be
+ * sorted by a sortable column by clicking or tapping the column's
+ * default header. Programmatic sorting using the Grid#sort methods is
+ * not affected by this setting.
+ *
+ * @param sortable
+ * {@code true} if the user should be able to sort the
+ * column, {@code false} otherwise
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the data source of the Grid does not implement
+ * {@link Sortable}
+ * @throws IllegalStateException
+ * if the data source does not support sorting by the
+ * property associated with this column
+ */
+ public Column setSortable(boolean sortable) {
+ checkColumnIsAttached();
+
+ if (sortable) {
+ if (!(grid.datasource instanceof Sortable)) {
+ throw new IllegalStateException(
+ "Can't set column "
+ + toString()
+ + " sortable. The Container of Grid does not implement Sortable");
+ } else if (!((Sortable) grid.datasource)
+ .getSortableContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalStateException(
+ "Can't set column "
+ + toString()
+ + " sortable. Container doesn't support sorting by property "
+ + propertyId);
+ }
+ }
+
+ state.sortable = sortable;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns whether the user can sort the grid by this column.
+ * <p>
+ * <em>Note:</em> it is possible to sort by this column programmatically
+ * using the Grid#sort methods regardless of the returned value.
+ *
+ * @return {@code true} if the column is sortable by the user,
+ * {@code false} otherwise
+ */
+ public boolean isSortable() {
+ return state.sortable;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[propertyId:"
+ + grid.getPropertyIdByColumnId(state.id) + "]";
+ }
+
+ /**
+ * Sets the ratio with which the column expands.
+ * <p>
+ * By default, all columns expand equally (treated as if all of them had
+ * an expand ratio of 1). Once at least one column gets a defined expand
+ * ratio, the implicit expand ratio is removed, and only the defined
+ * expand ratios are taken into account.
+ * <p>
+ * If a column has a defined width ({@link #setWidth(double)}), it
+ * overrides this method's effects.
+ * <p>
+ * <em>Example:</em> A grid with three columns, with expand ratios 0, 1
+ * and 2, respectively. The column with a <strong>ratio of 0 is exactly
+ * as wide as its contents requires</strong>. The column with a ratio of
+ * 1 is as wide as it needs, <strong>plus a third of any excess
+ * space</strong>, because we have 3 parts total, and this column
+ * reserves only one of those. The column with a ratio of 2, is as wide
+ * as it needs to be, <strong>plus two thirds</strong> of the excess
+ * width.
+ *
+ * @param expandRatio
+ * the expand ratio of this column. {@code 0} to not have it
+ * expand at all. A negative number to clear the expand
+ * value.
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setWidth(double)
+ */
+ public Column setExpandRatio(int expandRatio)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+
+ getState().expandRatio = expandRatio;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the column's expand ratio.
+ *
+ * @return the column's expand ratio
+ * @see #setExpandRatio(int)
+ */
+ public int getExpandRatio() {
+ return getState().expandRatio;
+ }
+
+ /**
+ * Clears the expand ratio for this column.
+ * <p>
+ * Equal to calling {@link #setExpandRatio(int) setExpandRatio(-1)}
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column clearExpandRatio() throws IllegalStateException {
+ return setExpandRatio(-1);
+ }
+
+ /**
+ * Sets the minimum width for this column.
+ * <p>
+ * This defines the minimum guaranteed pixel width of the column
+ * <em>when it is set to expand</em>.
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setExpandRatio(int)
+ */
+ public Column setMinimumWidth(double pixels)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+
+ final double maxwidth = getMaximumWidth();
+ if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) {
+ throw new IllegalArgumentException("New minimum width ("
+ + pixels + ") was greater than maximum width ("
+ + maxwidth + ")");
+ }
+ getState().minWidth = pixels;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Return the minimum width for this column.
+ *
+ * @return the minimum width for this column
+ * @see #setMinimumWidth(double)
+ */
+ public double getMinimumWidth() {
+ return getState().minWidth;
+ }
+
+ /**
+ * Sets the maximum width for this column.
+ * <p>
+ * This defines the maximum allowed pixel width of the column
+ * <em>when it is set to expand</em>.
+ *
+ * @param pixels
+ * the maximum width
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setExpandRatio(int)
+ */
+ public Column setMaximumWidth(double pixels) {
+ checkColumnIsAttached();
+
+ final double minwidth = getMinimumWidth();
+ if (pixels >= 0 && pixels < minwidth && minwidth >= 0) {
+ throw new IllegalArgumentException("New maximum width ("
+ + pixels + ") was less than minimum width (" + minwidth
+ + ")");
+ }
+
+ getState().maxWidth = pixels;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the maximum width for this column.
+ *
+ * @return the maximum width for this column
+ * @see #setMaximumWidth(double)
+ */
+ public double getMaximumWidth() {
+ return getState().maxWidth;
+ }
+
+ /**
+ * Sets whether the properties corresponding to this column should be
+ * editable when the item editor is active. By default columns are
+ * editable.
+ * <p>
+ * Values in non-editable columns are currently not displayed when the
+ * editor is active, but this will probably change in the future. They
+ * are not automatically assigned an editor field and, if one is
+ * manually assigned, it is not used. Columns that cannot (or should
+ * not) be edited even in principle should be set non-editable.
+ *
+ * @param editable
+ * {@code true} if this column should be editable,
+ * {@code false} otherwise
+ * @return this column
+ *
+ * @throws IllegalStateException
+ * if the editor is currently active
+ *
+ * @see Grid#editItem(Object)
+ * @see Grid#isEditorActive()
+ */
+ public Column setEditable(boolean editable) {
+ checkColumnIsAttached();
+ if (grid.isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot change column editable status while the editor is active");
+ }
+ getState().editable = editable;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns whether the properties corresponding to this column should be
+ * editable when the item editor is active.
+ *
+ * @return {@code true} if this column is editable, {@code false}
+ * otherwise
+ *
+ * @see Grid#editItem(Object)
+ * @see #setEditable(boolean)
+ */
+
+ public boolean isEditable() {
+ return getState().editable;
+ }
+
+ /**
+ * Sets the field component used to edit the properties in this column
+ * when the item editor is active. If an item has not been set, then the
+ * binding is postponed until the item is set using
+ * {@link #editItem(Object)}.
+ * <p>
+ * Setting the field to <code>null</code> clears any previously set
+ * field, causing a new field to be created the next time the item
+ * editor is opened.
+ *
+ * @param editor
+ * the editor field
+ * @return this column
+ */
+ public Column setEditorField(Field<?> editor) {
+ grid.setEditorField(getPropertyId(), editor);
+ return this;
+ }
+
+ /**
+ * Returns the editor field used to edit the properties in this column
+ * when the item editor is active. Returns null if the column is not
+ * {@link Column#isEditable() editable}.
+ * <p>
+ * When {@link #editItem(Object) editItem} is called, fields are
+ * automatically created and bound for any unbound properties.
+ * <p>
+ * Getting a field before the editor has been opened depends on special
+ * support from the {@link FieldGroup} in use. Using this method with a
+ * user-provided <code>FieldGroup</code> might cause
+ * {@link com.vaadin.data.fieldgroup.FieldGroup.BindException
+ * BindException} to be thrown.
+ *
+ * @return the bound field; or <code>null</code> if the respective
+ * column is not editable
+ *
+ * @throws IllegalArgumentException
+ * if there is no column for the provided property id
+ * @throws FieldGroup.BindException
+ * if no field has been configured and there is a problem
+ * building or binding
+ */
+ public Field<?> getEditorField() {
+ return grid.getEditorField(getPropertyId());
+ }
+
+ /**
+ * Hides or shows the column. By default columns are visible before
+ * explicitly hiding them.
+ *
+ * @since 7.5.0
+ * @param hidden
+ * <code>true</code> to hide the column, <code>false</code>
+ * to show
+ * @return this column
+ */
+ public Column setHidden(boolean hidden) {
+ if (hidden != getState().hidden) {
+ getState().hidden = hidden;
+ grid.markAsDirty();
+ grid.fireColumnVisibilityChangeEvent(this, hidden, false);
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column is hidden. Default is {@code false}.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the column is currently hidden,
+ * <code>false</code> otherwise
+ */
+ public boolean isHidden() {
+ return getState().hidden;
+ }
+
+ /**
+ * Sets whether this column can be hidden by the user. Hidable columns
+ * can be hidden and shown via the sidebar menu.
+ *
+ * @since 7.5.0
+ * @param hidable
+ * <code>true</code> iff the column may be hidable by the
+ * user via UI interaction
+ * @return this column
+ */
+ public Column setHidable(boolean hidable) {
+ if (hidable != getState().hidable) {
+ getState().hidable = hidable;
+ grid.markAsDirty();
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column can be hidden by the user. Default is
+ * {@code false}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically hidden using
+ * {@link #setHidden(boolean)} regardless of the returned value.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the user can hide the column,
+ * <code>false</code> if not
+ */
+ public boolean isHidable() {
+ return getState().hidable;
+ }
+
+ /**
+ * Sets whether this column can be resized by the user.
+ *
+ * @since 7.6
+ * @param resizable
+ * {@code true} if this column should be resizable,
+ * {@code false} otherwise
+ */
+ public Column setResizable(boolean resizable) {
+ if (resizable != getState().resizable) {
+ getState().resizable = resizable;
+ grid.markAsDirty();
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column can be resized by the user. Default is
+ * {@code true}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically resized using
+ * {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless
+ * of the returned value.
+ *
+ * @since 7.6
+ * @return {@code true} if this column is resizable, {@code false}
+ * otherwise
+ */
+ public boolean isResizable() {
+ return getState().resizable;
+ }
+
+ /**
+ * Writes the design attributes for this column into given element.
+ *
+ * @since 7.5.0
+ *
+ * @param design
+ * Element to write attributes into
+ *
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element design, DesignContext designContext) {
+ Attributes attributes = design.attributes();
+ GridColumnState def = new GridColumnState();
+
+ DesignAttributeHandler.writeAttribute("property-id", attributes,
+ getPropertyId(), null, Object.class);
+
+ // Sortable is a special attribute that depends on the container.
+ DesignAttributeHandler.writeAttribute("sortable", attributes,
+ isSortable(), null, boolean.class);
+ DesignAttributeHandler.writeAttribute("editable", attributes,
+ isEditable(), def.editable, boolean.class);
+ DesignAttributeHandler.writeAttribute("resizable", attributes,
+ isResizable(), def.resizable, boolean.class);
+
+ DesignAttributeHandler.writeAttribute("hidable", attributes,
+ isHidable(), def.hidable, boolean.class);
+ DesignAttributeHandler.writeAttribute("hidden", attributes,
+ isHidden(), def.hidden, boolean.class);
+ DesignAttributeHandler.writeAttribute("hiding-toggle-caption",
+ attributes, getHidingToggleCaption(), null, String.class);
+
+ DesignAttributeHandler.writeAttribute("width", attributes,
+ getWidth(), def.width, Double.class);
+ DesignAttributeHandler.writeAttribute("min-width", attributes,
+ getMinimumWidth(), def.minWidth, Double.class);
+ DesignAttributeHandler.writeAttribute("max-width", attributes,
+ getMaximumWidth(), def.maxWidth, Double.class);
+ DesignAttributeHandler.writeAttribute("expand", attributes,
+ getExpandRatio(), def.expandRatio, Integer.class);
+ }
+
+ /**
+ * Reads the design attributes for this column from given element.
+ *
+ * @since 7.5.0
+ * @param design
+ * Element to read attributes from
+ * @param designContext
+ * the design context
+ */
+ protected void readDesign(Element design, DesignContext designContext) {
+ Attributes attributes = design.attributes();
+
+ if (design.hasAttr("sortable")) {
+ setSortable(DesignAttributeHandler.readAttribute("sortable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("editable")) {
+ setEditable(DesignAttributeHandler.readAttribute("editable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("resizable")) {
+ setResizable(DesignAttributeHandler.readAttribute("resizable",
+ attributes, boolean.class));
+ }
+
+ if (design.hasAttr("hidable")) {
+ setHidable(DesignAttributeHandler.readAttribute("hidable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("hidden")) {
+ setHidden(DesignAttributeHandler.readAttribute("hidden",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("hiding-toggle-caption")) {
+ setHidingToggleCaption(DesignAttributeHandler.readAttribute(
+ "hiding-toggle-caption", attributes, String.class));
+ }
+
+ // Read size info where necessary.
+ if (design.hasAttr("width")) {
+ setWidth(DesignAttributeHandler.readAttribute("width",
+ attributes, Double.class));
+ }
+ if (design.hasAttr("min-width")) {
+ setMinimumWidth(DesignAttributeHandler.readAttribute(
+ "min-width", attributes, Double.class));
+ }
+ if (design.hasAttr("max-width")) {
+ setMaximumWidth(DesignAttributeHandler.readAttribute(
+ "max-width", attributes, Double.class));
+ }
+ if (design.hasAttr("expand")) {
+ if (design.attr("expand").isEmpty()) {
+ setExpandRatio(1);
+ } else {
+ setExpandRatio(DesignAttributeHandler.readAttribute(
+ "expand", attributes, Integer.class));
+ }
+ }
+ }
+ }
+
+ /**
+ * An abstract base class for server-side
+ * {@link com.vaadin.ui.renderers.Renderer Grid renderers}. This class
+ * currently extends the AbstractExtension superclass, but this fact should
+ * be regarded as an implementation detail and subject to change in a future
+ * major or minor Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ */
+ public static abstract class AbstractRenderer<T> extends
+ AbstractGridExtension implements Renderer<T> {
+
+ private final Class<T> presentationType;
+
+ private final String nullRepresentation;
+
+ protected AbstractRenderer(Class<T> presentationType,
+ String nullRepresentation) {
+ this.presentationType = presentationType;
+ this.nullRepresentation = nullRepresentation;
+ }
+
+ protected AbstractRenderer(Class<T> presentationType) {
+ this(presentationType, null);
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected Class<Grid> getSupportedParentType() {
+ return Grid.class;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ }
+
+ @Override
+ public Class<T> getPresentationType() {
+ return presentationType;
+ }
+
+ @Override
+ public JsonValue encode(T value) {
+ if (value == null) {
+ return encode(getNullRepresentation(), String.class);
+ } else {
+ return encode(value, getPresentationType());
+ }
+ }
+
+ /**
+ * Null representation for the renderer
+ *
+ * @return a textual representation of {@code null}
+ */
+ protected String getNullRepresentation() {
+ return nullRepresentation;
+ }
+
+ /**
+ * Encodes the given value to JSON.
+ * <p>
+ * This is a helper method that can be invoked by an
+ * {@link #encode(Object) encode(T)} override if serializing a value of
+ * type other than {@link #getPresentationType() the presentation type}
+ * is desired. For instance, a {@code Renderer<Date>} could first turn a
+ * date value into a formatted string and return
+ * {@code encode(dateString, String.class)}.
+ *
+ * @param value
+ * the value to be encoded
+ * @param type
+ * the type of the value
+ * @return a JSON representation of the given value
+ */
+ protected <U> JsonValue encode(U value, Class<U> type) {
+ return JsonCodec.encode(value, null, type,
+ getUI().getConnectorTracker()).getEncodedValue();
+ }
+
+ /**
+ * Converts and encodes the given data model property value using the
+ * given converter and renderer. This method is public only for testing
+ * purposes.
+ *
+ * @since 7.6
+ * @param renderer
+ * the renderer to use
+ * @param converter
+ * the converter to use
+ * @param modelValue
+ * the value to convert and encode
+ * @param locale
+ * the locale to use in conversion
+ * @return an encoded value ready to be sent to the client
+ */
+ public static <T> JsonValue encodeValue(Object modelValue,
+ Renderer<T> renderer, Converter<?, ?> converter, Locale locale) {
+ Class<T> presentationType = renderer.getPresentationType();
+ T presentationValue;
+
+ if (converter == null) {
+ try {
+ presentationValue = presentationType.cast(modelValue);
+ } catch (ClassCastException e) {
+ if (presentationType == String.class) {
+ // If there is no converter, just fallback to using
+ // toString(). modelValue can't be null as
+ // Class.cast(null) will always succeed
+ presentationValue = (T) modelValue.toString();
+ } else {
+ throw new Converter.ConversionException(
+ "Unable to convert value of type "
+ + modelValue.getClass().getName()
+ + " to presentation type "
+ + presentationType.getName()
+ + ". No converter is set and the types are not compatible.");
+ }
+ }
+ } else {
+ assert presentationType.isAssignableFrom(converter
+ .getPresentationType());
+ @SuppressWarnings("unchecked")
+ Converter<T, Object> safeConverter = (Converter<T, Object>) converter;
+ presentationValue = safeConverter
+ .convertToPresentation(modelValue,
+ safeConverter.getPresentationType(), locale);
+ }
+
+ JsonValue encodedValue;
+ try {
+ encodedValue = renderer.encode(presentationValue);
+ } catch (Exception e) {
+ getLogger().log(Level.SEVERE, "Unable to encode data", e);
+ encodedValue = renderer.encode(null);
+ }
+
+ return encodedValue;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(AbstractRenderer.class.getName());
+ }
+
+ }
+
+ /**
+ * An abstract base class for server-side Grid extensions.
+ * <p>
+ * Note: If the extension is an instance of {@link DataGenerator} it will
+ * automatically register itself to {@link RpcDataProviderExtension} of
+ * extended Grid. On remove this registration is automatically removed.
+ *
+ * @since 7.5
+ */
+ public static abstract class AbstractGridExtension extends
+ AbstractExtension {
+
+ /**
+ * Constructs a new Grid extension.
+ */
+ public AbstractGridExtension() {
+ super();
+ }
+
+ /**
+ * Constructs a new Grid extension and extends given Grid.
+ *
+ * @param grid
+ * a grid instance
+ */
+ public AbstractGridExtension(Grid grid) {
+ super();
+ extend(grid);
+ }
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+
+ if (this instanceof DataGenerator) {
+ getParentGrid().datasourceExtension
+ .addDataGenerator((DataGenerator) this);
+ }
+ }
+
+ @Override
+ public void remove() {
+ if (this instanceof DataGenerator) {
+ getParentGrid().datasourceExtension
+ .removeDataGenerator((DataGenerator) this);
+ }
+
+ super.remove();
+ }
+
+ /**
+ * Gets the item id for a row key.
+ * <p>
+ * A key is used to identify a particular row on both a server and a
+ * client. This method can be used to get the item id for the row key
+ * that the client has sent.
+ *
+ * @param rowKey
+ * the row key for which to retrieve an item id
+ * @return the item id corresponding to {@code key}
+ */
+ protected Object getItemId(String rowKey) {
+ return getParentGrid().getKeyMapper().get(rowKey);
+ }
+
+ /**
+ * Gets the column for a column id.
+ * <p>
+ * An id is used to identify a particular column on both a server and a
+ * client. This method can be used to get the column for the column id
+ * that the client has sent.
+ *
+ * @param columnId
+ * the column id for which to retrieve a column
+ * @return the column corresponding to {@code columnId}
+ */
+ protected Column getColumn(String columnId) {
+ return getParentGrid().getColumnByColumnId(columnId);
+ }
+
+ /**
+ * Gets the parent Grid of the renderer.
+ *
+ * @return parent grid
+ * @throws IllegalStateException
+ * if parent is not Grid
+ */
+ protected Grid getParentGrid() {
+ if (getParent() instanceof Grid) {
+ Grid grid = (Grid) getParent();
+ return grid;
+ } else if (getParent() == null) {
+ throw new IllegalStateException(
+ "Renderer is not attached to any parent");
+ } else {
+ throw new IllegalStateException(
+ "Renderers can be used only with Grid. Extended "
+ + getParent().getClass().getSimpleName()
+ + " instead");
+ }
+ }
+
+ /**
+ * Resends the row data for given item id to the client.
+ *
+ * @since 7.6
+ * @param itemId
+ * row to refresh
+ */
+ protected void refreshRow(Object itemId) {
+ getParentGrid().datasourceExtension.updateRowData(itemId);
+ }
+
+ /**
+ * Informs the parent Grid that this Extension wants to add a child
+ * component to it.
+ *
+ * @since 7.6
+ * @param c
+ * component
+ */
+ protected void addComponentToGrid(Component c) {
+ getParentGrid().addComponent(c);
+ }
+
+ /**
+ * Informs the parent Grid that this Extension wants to remove a child
+ * component from it.
+ *
+ * @since 7.6
+ * @param c
+ * component
+ */
+ protected void removeComponentFromGrid(Component c) {
+ getParentGrid().removeComponent(c);
+ }
+ }
+
+ /**
+ * The data source attached to the grid
+ */
+ private Container.Indexed datasource;
+
+ /**
+ * Property id to column instance mapping
+ */
+ private final Map<Object, Column> columns = new HashMap<Object, Column>();
+
+ /**
+ * Key generator for column server-to-client communication
+ */
+ private final KeyMapper<Object> columnKeys = new KeyMapper<Object>();
+
+ /**
+ * The current sort order
+ */
+ private final List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ /**
+ * Property listener for listening to changes in data source properties.
+ */
+ private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() {
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ Collection<?> properties = new HashSet<Object>(event.getContainer()
+ .getContainerPropertyIds());
+
+ // Find columns that need to be removed.
+ List<Column> removedColumns = new LinkedList<Column>();
+ for (Object propertyId : columns.keySet()) {
+ if (!properties.contains(propertyId)) {
+ removedColumns.add(getColumn(propertyId));
+ }
+ }
+
+ // Actually remove columns.
+ for (Column column : removedColumns) {
+ Object propertyId = column.getPropertyId();
+ internalRemoveColumn(propertyId);
+ columnKeys.remove(propertyId);
+ }
+ datasourceExtension.columnsRemoved(removedColumns);
+
+ // Add new columns
+ List<Column> addedColumns = new LinkedList<Column>();
+ for (Object propertyId : properties) {
+ if (!columns.containsKey(propertyId)) {
+ addedColumns.add(appendColumn(propertyId));
+ }
+ }
+ datasourceExtension.columnsAdded(addedColumns);
+
+ if (getFrozenColumnCount() > columns.size()) {
+ setFrozenColumnCount(columns.size());
+ }
+
+ // Unset sortable for non-sortable columns.
+ if (datasource instanceof Sortable) {
+ Collection<?> sortables = ((Sortable) datasource)
+ .getSortableContainerPropertyIds();
+ for (Object propertyId : columns.keySet()) {
+ Column column = columns.get(propertyId);
+ if (!sortables.contains(propertyId) && column.isSortable()) {
+ column.setSortable(false);
+ }
+ }
+ }
+ }
+ };
+
+ private final ItemSetChangeListener editorClosingItemSetListener = new ItemSetChangeListener() {
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ cancelEditor();
+ }
+ };
+
+ private RpcDataProviderExtension datasourceExtension;
+
+ /**
+ * The selection model that is currently in use. Never <code>null</code>
+ * after the constructor has been run.
+ */
+ private SelectionModel selectionModel;
+
+ /**
+ * Used to know whether selection change events originate from the server or
+ * the client so the selection change handler knows whether the changes
+ * should be sent to the client.
+ */
+ private boolean applyingSelectionFromClient;
+
+ private final Header header = new Header(this);
+ private final Footer footer = new Footer(this);
+
+ private Object editedItemId = null;
+ private boolean editorActive = false;
+ private FieldGroup editorFieldGroup = new CustomFieldGroup();
+
+ private CellStyleGenerator cellStyleGenerator;
+ private RowStyleGenerator rowStyleGenerator;
+
+ private CellDescriptionGenerator cellDescriptionGenerator;
+ private RowDescriptionGenerator rowDescriptionGenerator;
+
+ /**
+ * <code>true</code> if Grid is using the internal IndexedContainer created
+ * in Grid() constructor, or <code>false</code> if the user has set their
+ * own Container.
+ *
+ * @see #setContainerDataSource(Indexed)
+ * @see #Grid()
+ */
+ private boolean defaultContainer = true;
+
+ private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
+
+ private DetailComponentManager detailComponentManager = null;
+
+ private Set<Component> extensionComponents = new HashSet<Component>();
+
+ private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+ .findMethod(SelectionListener.class, "select", SelectionEvent.class);
+
+ private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
+ .findMethod(SortListener.class, "sort", SortEvent.class);
+
+ private static final Method COLUMN_REORDER_METHOD = ReflectTools
+ .findMethod(ColumnReorderListener.class, "columnReorder",
+ ColumnReorderEvent.class);
+
+ private static final Method COLUMN_RESIZE_METHOD = ReflectTools
+ .findMethod(ColumnResizeListener.class, "columnResize",
+ ColumnResizeEvent.class);
+
+ private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools
+ .findMethod(ColumnVisibilityChangeListener.class,
+ "columnVisibilityChanged",
+ ColumnVisibilityChangeEvent.class);
+
+ /**
+ * Creates a new Grid with a new {@link IndexedContainer} as the data
+ * source.
+ */
+ public Grid() {
+ this(null, null);
+ }
+
+ /**
+ * Creates a new Grid using the given data source.
+ *
+ * @param dataSource
+ * the indexed container to use as a data source
+ */
+ public Grid(final Container.Indexed dataSource) {
+ this(null, dataSource);
+ }
+
+ /**
+ * Creates a new Grid with the given caption and a new
+ * {@link IndexedContainer} data source.
+ *
+ * @param caption
+ * the caption of the grid
+ */
+ public Grid(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * Creates a new Grid with the given caption and data source. If the data
+ * source is null, a new {@link IndexedContainer} will be used.
+ *
+ * @param caption
+ * the caption of the grid
+ * @param dataSource
+ * the indexed container to use as a data source
+ */
+ public Grid(String caption, Container.Indexed dataSource) {
+ if (dataSource == null) {
+ internalSetContainerDataSource(new IndexedContainer());
+ } else {
+ setContainerDataSource(dataSource);
+ }
+ setCaption(caption);
+ initGrid();
+ }
+
+ /**
+ * Grid initial setup
+ */
+ private void initGrid() {
+ setSelectionMode(getDefaultSelectionMode());
+
+ registerRpc(new GridServerRpc() {
+
+ @Override
+ public void sort(String[] columnIds, SortDirection[] directions,
+ boolean userOriginated) {
+ assert columnIds.length == directions.length;
+
+ List<SortOrder> order = new ArrayList<SortOrder>(
+ columnIds.length);
+ for (int i = 0; i < columnIds.length; i++) {
+ Object propertyId = getPropertyIdByColumnId(columnIds[i]);
+ order.add(new SortOrder(propertyId, directions[i]));
+ }
+ setSortOrder(order, userOriginated);
+ if (!order.equals(getSortOrder())) {
+ /*
+ * Actual sort order is not what the client expects. Make
+ * sure the client gets a state change event by clearing the
+ * diffstate and marking as dirty
+ */
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker
+ .getDiffState(Grid.this);
+ diffState.remove("sortColumns");
+ diffState.remove("sortDirs");
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void itemClick(String rowKey, String columnId,
+ MouseEventDetails details) {
+ Object itemId = getKeyMapper().get(rowKey);
+ Item item = datasource.getItem(itemId);
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ fireEvent(new ItemClickEvent(Grid.this, item, itemId,
+ propertyId, details));
+ }
+
+ @Override
+ public void columnsReordered(List<String> newColumnOrder,
+ List<String> oldColumnOrder) {
+ final String diffStateKey = "columnOrder";
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker.getDiffState(Grid.this);
+ // discard the change if the columns have been reordered from
+ // the server side, as the server side is always right
+ if (getState(false).columnOrder.equals(oldColumnOrder)) {
+ // Don't mark as dirty since client has the state already
+ getState(false).columnOrder = newColumnOrder;
+ // write changes to diffState so that possible reverting the
+ // column order is sent to client
+ assert diffState.hasKey(diffStateKey) : "Field name has changed";
+ Type type = null;
+ try {
+ type = (getState(false).getClass().getDeclaredField(
+ diffStateKey).getGenericType());
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ EncodeResult encodeResult = JsonCodec.encode(
+ getState(false).columnOrder, diffState, type,
+ connectorTracker);
+
+ diffState.put(diffStateKey, encodeResult.getEncodedValue());
+ fireColumnReorderEvent(true);
+ } else {
+ // make sure the client is reverted to the order that the
+ // server thinks it is
+ diffState.remove(diffStateKey);
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void columnVisibilityChanged(String id, boolean hidden,
+ boolean userOriginated) {
+ final Column column = getColumnByColumnId(id);
+ final GridColumnState columnState = column.getState();
+
+ if (columnState.hidden != hidden) {
+ columnState.hidden = hidden;
+
+ final String diffStateKey = "columns";
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker
+ .getDiffState(Grid.this);
+
+ assert diffState.hasKey(diffStateKey) : "Field name has changed";
+ Type type = null;
+ try {
+ type = (getState(false).getClass().getDeclaredField(
+ diffStateKey).getGenericType());
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ EncodeResult encodeResult = JsonCodec.encode(
+ getState(false).columns, diffState, type,
+ connectorTracker);
+
+ diffState.put(diffStateKey, encodeResult.getEncodedValue());
+
+ fireColumnVisibilityChangeEvent(column, hidden,
+ userOriginated);
+ }
+ }
+
+ @Override
+ public void contextClick(int rowIndex, String rowKey,
+ String columnId, Section section, MouseEventDetails details) {
+ Object itemId = null;
+ if (rowKey != null) {
+ itemId = getKeyMapper().get(rowKey);
+ }
+ fireEvent(new GridContextClickEvent(Grid.this, details,
+ section, rowIndex, itemId,
+ getPropertyIdByColumnId(columnId)));
+ }
+
+ @Override
+ public void columnResized(String id, double pixels) {
+ final Column column = getColumnByColumnId(id);
+ if (column != null && column.isResizable()) {
+ column.getState().width = pixels;
+ fireColumnResizeEvent(column, true);
+ markAsDirty();
+ }
+ }
+ });
+
+ registerRpc(new EditorServerRpc() {
+
+ @Override
+ public void bind(int rowIndex) {
+ try {
+ Object id = getContainerDataSource().getIdByIndex(rowIndex);
+
+ final boolean opening = editedItemId == null;
+
+ final boolean moving = !opening && !editedItemId.equals(id);
+
+ final boolean allowMove = !isEditorBuffered()
+ && getEditorFieldGroup().isValid();
+
+ if (opening || !moving || allowMove) {
+ doBind(id);
+ } else {
+ failBind(null);
+ }
+ } catch (Exception e) {
+ failBind(e);
+ }
+ }
+
+ private void doBind(Object id) {
+ editedItemId = id;
+ doEditItem();
+ getEditorRpc().confirmBind(true);
+ }
+
+ private void failBind(Exception e) {
+ if (e != null) {
+ handleError(e);
+ }
+ getEditorRpc().confirmBind(false);
+ }
+
+ @Override
+ public void cancel(int rowIndex) {
+ try {
+ // For future proofing even though cannot currently fail
+ doCancelEditor();
+ } catch (Exception e) {
+ handleError(e);
+ }
+ }
+
+ @Override
+ public void save(int rowIndex) {
+ List<String> errorColumnIds = null;
+ String errorMessage = null;
+ boolean success = false;
+ try {
+ saveEditor();
+ success = true;
+ } catch (CommitException e) {
+ try {
+ CommitErrorEvent event = new CommitErrorEvent(
+ Grid.this, e);
+ getEditorErrorHandler().commitError(event);
+
+ errorMessage = event.getUserErrorMessage();
+
+ errorColumnIds = new ArrayList<String>();
+ for (Column column : event.getErrorColumns()) {
+ errorColumnIds.add(column.state.id);
+ }
+ } catch (Exception ee) {
+ // A badly written error handler can throw an exception,
+ // which would lock up the Grid
+ handleError(ee);
+ }
+ } catch (Exception e) {
+ handleError(e);
+ }
+ getEditorRpc().confirmSave(success, errorMessage,
+ errorColumnIds);
+ }
+
+ private void handleError(Exception e) {
+ com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this).error(
+ new ConnectorErrorEvent(Grid.this, e));
+ }
+ });
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ try {
+ header.sanityCheck();
+ footer.sanityCheck();
+ } catch (Exception e) {
+ e.printStackTrace();
+ setComponentError(new ErrorMessage() {
+
+ @Override
+ public ErrorLevel getErrorLevel() {
+ return ErrorLevel.CRITICAL;
+ }
+
+ @Override
+ public String getFormattedHtmlMessage() {
+ return "Incorrectly merged cells";
+ }
+
+ });
+ }
+
+ super.beforeClientResponse(initial);
+ }
+
+ /**
++ * Sets the grid data source.<p>
++ *
++ * <strong>Note</strong> Grid columns are based on properties and try to detect a correct converter for
++ * the data type. The columns are not reinitialized automatically if the container is changed, and if the same
++ * properties are present after container change, the columns are reused.
++ * Properties with same names, but different data types will lead to unpredictable behaviour.
++ *
+ * @param container
+ * The container data source. Cannot be null.
+ * @throws IllegalArgumentException
+ * if the data source is null
+ */
+ public void setContainerDataSource(Container.Indexed container) {
+ defaultContainer = false;
+ internalSetContainerDataSource(container);
+ }
+
+ private void internalSetContainerDataSource(Container.Indexed container) {
+ if (container == null) {
+ throw new IllegalArgumentException(
+ "Cannot set the datasource to null");
+ }
+ if (datasource == container) {
+ return;
+ }
+
+ // Remove old listeners
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .removePropertySetChangeListener(propertyListener);
+ }
+
+ if (datasourceExtension != null) {
+ removeExtension(datasourceExtension);
+ }
+
+ // Remove old DetailComponentManager
+ if (detailComponentManager != null) {
+ detailComponentManager.remove();
+ }
+
+ resetEditor();
+
+ datasource = container;
+
+ //
+ // Adjust sort order
+ //
+
+ if (container instanceof Container.Sortable) {
+
+ // If the container is sortable, go through the current sort order
+ // and match each item to the sortable properties of the new
+ // container. If the new container does not support an item in the
+ // current sort order, that item is removed from the current sort
+ // order list.
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
+ .getSortableContainerPropertyIds();
+
+ Iterator<SortOrder> i = sortOrder.iterator();
+ while (i.hasNext()) {
+ if (!sortableProps.contains(i.next().getPropertyId())) {
+ i.remove();
+ }
+ }
+
+ sort(false);
+ } else {
+ // Clear sorting order. Don't sort.
+ sortOrder.clear();
+ }
+
+ datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension.extend(this);
+ datasourceExtension.addDataGenerator(new RowDataGenerator());
+ for (Extension e : getExtensions()) {
+ if (e instanceof DataGenerator) {
+ datasourceExtension.addDataGenerator((DataGenerator) e);
+ }
+ }
+
+ if (detailComponentManager != null) {
+ detailComponentManager = new DetailComponentManager(this,
+ detailComponentManager.getDetailsGenerator());
+ } else {
+ detailComponentManager = new DetailComponentManager(this);
+ }
+
+ /*
+ * selectionModel == null when the invocation comes from the
+ * constructor.
+ */
+ if (selectionModel != null) {
+ selectionModel.reset();
+ }
+
+ // Listen to changes in properties and remove columns if needed
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .addPropertySetChangeListener(propertyListener);
+ }
+
+ /*
+ * activeRowHandler will be updated by the client-side request that
+ * occurs on container change - no need to actively re-insert any
+ * ValueChangeListeners at this point.
+ */
+
+ setFrozenColumnCount(0);
+
+ if (columns.isEmpty()) {
+ // Add columns
+ for (Object propertyId : datasource.getContainerPropertyIds()) {
+ Column column = appendColumn(propertyId);
+
+ // Initial sorting is defined by container
+ if (datasource instanceof Sortable) {
+ column.setSortable(((Sortable) datasource)
+ .getSortableContainerPropertyIds().contains(
+ propertyId));
+ } else {
+ column.setSortable(false);
+ }
+ }
+ } else {
+ Collection<?> properties = datasource.getContainerPropertyIds();
+ for (Object property : columns.keySet()) {
+ if (!properties.contains(property)) {
+ throw new IllegalStateException(
+ "Found at least one column in Grid that does not exist in the given container: "
+ + property
+ + " with the header \""
+ + getColumn(property).getHeaderCaption()
+ + "\"");
+ }
+
+ if (!(datasource instanceof Sortable)
+ || !((Sortable) datasource)
+ .getSortableContainerPropertyIds().contains(
+ property)) {
+ columns.get(property).setSortable(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the grid data source.
+ *
+ * @return the container data source of the grid
+ */
+ public Container.Indexed getContainerDataSource() {
+ return datasource;
+ }
+
+ /**
+ * Returns a column based on the property id
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the column or <code>null</code> if not found
+ */
+ public Column getColumn(Object propertyId) {
+ return columns.get(propertyId);
+ }
+
+ /**
+ * Returns a copy of currently configures columns in their current visual
+ * order in this Grid.
+ *
+ * @return unmodifiable copy of current columns in visual order
+ */
+ public List<Column> getColumns() {
+ List<Column> columns = new ArrayList<Grid.Column>();
+ for (String columnId : getState(false).columnOrder) {
+ columns.add(getColumnByColumnId(columnId));
+ }
+ return Collections.unmodifiableList(columns);
+ }
+
+ /**
+ * Adds a new Column to Grid. Also adds the property to container with data
+ * type String, if property for column does not exist in it. Default value
+ * for the new property is an empty String.
+ * <p>
+ * Note that adding a new property is only done for the default container
+ * that Grid sets up with the default constructor.
+ *
+ * @param propertyId
+ * the property id of the new column
+ * @return the new column
+ *
+ * @throws IllegalStateException
+ * if column for given property already exists in this grid
+ */
+
+ public Column addColumn(Object propertyId) throws IllegalStateException {
+ if (datasource.getContainerPropertyIds().contains(propertyId)
+ && !columns.containsKey(propertyId)) {
+ appendColumn(propertyId);
+ } else if (defaultContainer) {
+ addColumnProperty(propertyId, String.class, "");
+ } else {
+ if (columns.containsKey(propertyId)) {
+ throw new IllegalStateException("A column for property id '"
+ + propertyId.toString()
+ + "' already exists in this grid");
+ } else {
+ throw new IllegalStateException("Property id '"
+ + propertyId.toString()
+ + "' does not exist in the container");
+ }
+ }
+
+ // Inform the data provider of this new column.
+ Column column = getColumn(propertyId);
+ List<Column> addedColumns = new ArrayList<Column>();
+ addedColumns.add(column);
+ datasourceExtension.columnsAdded(addedColumns);
+
+ return column;
+ }
+
+ /**
+ * Adds a new Column to Grid. This function makes sure that the property
+ * with the given id and data type exists in the container. If property does
+ * not exists, it will be created.
+ * <p>
+ * Default value for the new property is 0 if type is Integer, Double and
+ * Float. If type is String, default value is an empty string. For all other
+ * types the default value is null.
+ * <p>
+ * Note that adding a new property is only done for the default container
+ * that Grid sets up with the default constructor.
+ *
+ * @param propertyId
+ * the property id of the new column
+ * @param type
+ * the data type for the new property
+ * @return the new column
+ *
+ * @throws IllegalStateException
+ * if column for given property already exists in this grid or
+ * property already exists in the container with wrong type
+ */
+ public Column addColumn(Object propertyId, Class<?> type) {
+ addColumnProperty(propertyId, type, null);
+ return getColumn(propertyId);
+ }
+
+ protected void addColumnProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws IllegalStateException {
+ if (!defaultContainer) {
+ throw new IllegalStateException(
+ "Container for this Grid is not a default container from Grid() constructor");
+ }
+
+ if (!columns.containsKey(propertyId)) {
+ if (!datasource.getContainerPropertyIds().contains(propertyId)) {
+ datasource.addContainerProperty(propertyId, type, defaultValue);
+ } else {
+ Property<?> containerProperty = datasource
+ .getContainerProperty(datasource.firstItemId(),
+ propertyId);
+ if (containerProperty.getType() == type) {
+ appendColumn(propertyId);
+ } else {
+ throw new IllegalStateException(
+ "DataSource already has the given property "
+ + propertyId + " with a different type");
+ }
+ }
+ } else {
+ throw new IllegalStateException(
+ "Grid already has a column for property " + propertyId);
+ }
+ }
+
+ /**
+ * Removes all columns from this Grid.
+ */
+ public void removeAllColumns() {
+ List<Column> removed = new ArrayList<Column>(columns.values());
+ Set<Object> properties = new HashSet<Object>(columns.keySet());
+ for (Object propertyId : properties) {
+ removeColumn(propertyId);
+ }
+ datasourceExtension.columnsRemoved(removed);
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a {@link Column} by
+ * referencing its generated state id. Also used by {@link Column} to verify
+ * if it has been detached from the {@link Grid}.
+ *
+ * @param columnId
+ * the client id generated for the column when the column is
+ * added to the grid
+ * @return the column with the id or <code>null</code> if not found
+ */
+ Column getColumnByColumnId(String columnId) {
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ return getColumn(propertyId);
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a property id by referencing
+ * the columns generated state id.
+ *
+ * @param columnId
+ * The state id of the column
+ * @return The column instance or null if not found
+ */
+ Object getPropertyIdByColumnId(String columnId) {
+ return columnKeys.get(columnId);
+ }
+
+ /**
+ * Returns whether column reordering is allowed. Default value is
+ * <code>false</code>.
+ *
+ * @since 7.5.0
+ * @return true if reordering is allowed
+ */
+ public boolean isColumnReorderingAllowed() {
+ return getState(false).columnReorderingAllowed;
+ }
+
+ /**
+ * Sets whether or not column reordering is allowed. Default value is
+ * <code>false</code>.
+ *
+ * @since 7.5.0
+ * @param columnReorderingAllowed
+ * specifies whether column reordering is allowed
+ */
+ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
+ if (isColumnReorderingAllowed() != columnReorderingAllowed) {
+ getState().columnReorderingAllowed = columnReorderingAllowed;
+ }
+ }
+
+ @Override
+ protected GridState getState() {
+ return (GridState) super.getState();
+ }
+
+ @Override
+ protected GridState getState(boolean markAsDirty) {
+ return (GridState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Creates a new column based on a property id and appends it as the last
+ * column.
+ *
+ * @param datasourcePropertyId
+ * The property id of a property in the datasource
+ */
+ private Column appendColumn(Object datasourcePropertyId) {
+ if (datasourcePropertyId == null) {
+ throw new IllegalArgumentException("Property id cannot be null");
+ }
+ assert datasource.getContainerPropertyIds().contains(
+ datasourcePropertyId) : "Datasource should contain the property id";
+
+ GridColumnState columnState = new GridColumnState();
+ columnState.id = columnKeys.key(datasourcePropertyId);
+
+ Column column = new Column(this, columnState, datasourcePropertyId);
+ columns.put(datasourcePropertyId, column);
+
+ getState().columns.add(columnState);
+ getState().columnOrder.add(columnState.id);
+ header.addColumn(datasourcePropertyId);
+ footer.addColumn(datasourcePropertyId);
+
+ String humanFriendlyPropertyId = SharedUtil
+ .propertyIdToHumanFriendly(String.valueOf(datasourcePropertyId));
+ column.setHeaderCaption(humanFriendlyPropertyId);
+
+ if (datasource instanceof Sortable
+ && ((Sortable) datasource).getSortableContainerPropertyIds()
+ .contains(datasourcePropertyId)) {
+ column.setSortable(true);
+ }
+
+ return column;
+ }
+
+ /**
+ * Removes a column from Grid based on a property id.
+ *
+ * @param propertyId
+ * The property id of column to be removed
+ *
+ * @throws IllegalArgumentException
+ * if there is no column for given property id in this grid
+ */
+ public void removeColumn(Object propertyId) throws IllegalArgumentException {
+ if (!columns.keySet().contains(propertyId)) {
+ throw new IllegalArgumentException(
+ "There is no column for given property id " + propertyId);
+ }
+
+ List<Column> removed = new ArrayList<Column>();
+ removed.add(getColumn(propertyId));
+ internalRemoveColumn(propertyId);
+ datasourceExtension.columnsRemoved(removed);
+ }
+
+ private void internalRemoveColumn(Object propertyId) {
+ setEditorField(propertyId, null);
+ header.removeColumn(propertyId);
+ footer.removeColumn(propertyId);
+ Column column = columns.remove(propertyId);
+ getState().columnOrder.remove(columnKeys.key(propertyId));
+ getState().columns.remove(column.getState());
+ removeExtension(column.getRenderer());
+ }
+
+ /**
+ * Sets the columns and their order for the grid. Current columns whose
+ * property id is not in propertyIds are removed. Similarly, a column is
+ * added for any property id in propertyIds that has no corresponding column
+ * in this Grid.
+ *
+ * @since 7.5.0
+ *
+ * @param propertyIds
+ * properties in the desired column order
+ */
+ public void setColumns(Object... propertyIds) {
+ Set<?> removePids = new HashSet<Object>(columns.keySet());
+ removePids.removeAll(Arrays.asList(propertyIds));
+ for (Object removePid : removePids) {
+ removeColumn(removePid);
+ }
+ Set<?> addPids = new HashSet<Object>(Arrays.asList(propertyIds));
+ addPids.removeAll(columns.keySet());
+ for (Object propertyId : addPids) {
+ addColumn(propertyId);
+ }
+ setColumnOrder(propertyIds);
+ }
+
+ /**
+ * Sets a new column order for the grid. All columns which are not ordered
+ * here will remain in the order they were before as the last columns of
+ * grid.
+ *
+ * @param propertyIds
+ * properties in the order columns should be
+ */
+ public void setColumnOrder(Object... propertyIds) {
+ List<String> columnOrder = new ArrayList<String>();
+ for (Object propertyId : propertyIds) {
+ if (columns.containsKey(propertyId)) {
+ columnOrder.add(columnKeys.key(propertyId));
+ } else {
+ throw new IllegalArgumentException(
+ "Grid does not contain column for property "
+ + String.valueOf(propertyId));
+ }
+ }
+
+ List<String> stateColumnOrder = getState().columnOrder;
+ if (stateColumnOrder.size() != columnOrder.size()) {
+ stateColumnOrder.removeAll(columnOrder);
+ columnOrder.addAll(stateColumnOrder);
+ }
+ getState().columnOrder = columnOrder;
+ fireColumnReorderEvent(false);
+ }
+
+ /**
+ * Sets the number of frozen columns in this grid. Setting the count to 0
+ * means that no data columns will be frozen, but the built-in selection
+ * checkbox column will still be frozen if it's in use. Setting the count to
+ * -1 will also disable the selection column.
+ * <p>
+ * The default value is 0.
+ *
+ * @param numberOfColumns
+ * the number of columns that should be frozen
+ *
+ * @throws IllegalArgumentException
+ * if the column count is < 0 or > the number of visible columns
+ */
+ public void setFrozenColumnCount(int numberOfColumns) {
+ if (numberOfColumns < -1 || numberOfColumns > columns.size()) {
+ throw new IllegalArgumentException(
+ "count must be between -1 and the current number of columns ("
+ + columns.size() + "): " + numberOfColumns);
+ }
+
+ getState().frozenColumnCount = numberOfColumns;
+ }
+
+ /**
+ * Gets the number of frozen columns in this grid. 0 means that no data
+ * columns will be frozen, but the built-in selection checkbox column will
+ * still be frozen if it's in use. -1 means that not even the selection
+ * column is frozen.
+ * <p>
+ * <em>NOTE:</em> this count includes {@link Column#isHidden() hidden
+ * columns} in the count.
+ *
+ * @see #setFrozenColumnCount(int)
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount() {
+ return getState(false).frozenColumnCount;
+ }
+
+ /**
+ * Scrolls to a certain item, using {@link ScrollDestination#ANY}.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(Object itemId) throws IllegalArgumentException {
+ scrollTo(itemId, ScrollDestination.ANY);
+ }
+
+ /**
+ * Scrolls to a certain item, using user-specified scroll destination.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @param destination
+ * value specifying desired position of scrolled-to row.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(Object itemId, ScrollDestination destination)
+ throws IllegalArgumentException {
+
+ int row = datasource.indexOfId(itemId);
+
+ if (row == -1) {
+ throw new IllegalArgumentException(
+ "Item with specified ID does not exist in data source");
+ }
+
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToRow(row, destination);
+ }
+
+ /**
+ * Scrolls to the beginning of the first data row.
+ */
+ public void scrollToStart() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToStart();
+ }
+
+ /**
+ * Scrolls to the end of the last data row.
+ */
+ public void scrollToEnd() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToEnd();
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead. If <code>null</code> is given, then Grid's
+ * height is undefined
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInfinite(double) infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ */
+ public void setHeightByRows(double rows) {
+ if (rows <= 0.0d) {
+ throw new IllegalArgumentException(
+ "More than zero rows must be shown.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "Grid doesn't support infinite heights");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("NaN is not a valid row count");
+ }
+
+ getState().heightByRows = rows;
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ *
+ * @return the amount of rows that are being shown in Grid's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return getState(false).heightByRows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(float height, Unit unit) {
+ super.setHeight(height, unit);
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via a {@code setHeight}-method, and behave as a traditional Component.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight and setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ getState().heightMode = heightMode;
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return getState(false).heightMode;
+ }
+
+ /* Selection related methods: */
+
+ /**
+ * Takes a new {@link SelectionModel} into use.
+ * <p>
+ * The SelectionModel that is previously in use will have all its items
+ * deselected.
+ * <p>
+ * If the given SelectionModel is already in use, this method does nothing.
+ *
+ * @param selectionModel
+ * the new SelectionModel to use
+ * @throws IllegalArgumentException
+ * if {@code selectionModel} is <code>null</code>
+ */
+ public void setSelectionModel(SelectionModel selectionModel)
+ throws IllegalArgumentException {
+ if (selectionModel == null) {
+ throw new IllegalArgumentException(
+ "Selection model may not be null");
+ }
+
+ if (this.selectionModel != selectionModel) {
+ // this.selectionModel is null on init
+ if (this.selectionModel != null) {
+ this.selectionModel.remove();
+ }
+
+ this.selectionModel = selectionModel;
+ selectionModel.setGrid(this);
+ }
+ }
+
+ /**
+ * Returns the currently used {@link SelectionModel}.
+ *
+ * @return the currently used SelectionModel
+ */
+ public SelectionModel getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Sets the Grid's selection mode.
+ * <p>
+ * Grid supports three selection modes: multiselect, single select and no
+ * selection, and this is a convenience method for choosing between one of
+ * them.
+ * <p>
+ * Technically, this method is a shortcut that can be used instead of
+ * calling {@code setSelectionModel} with a specific SelectionModel
+ * instance. Grid comes with three built-in SelectionModel classes, and the
+ * {@link SelectionMode} enum represents each of them.
+ * <p>
+ * Essentially, the two following method calls are equivalent:
+ * <p>
+ * <code><pre>
+ * grid.setSelectionMode(SelectionMode.MULTI);
+ * grid.setSelectionModel(new MultiSelectionMode());
+ * </pre></code>
+ *
+ *
+ * @param selectionMode
+ * the selection mode to switch to
+ * @return The {@link SelectionModel} instance that was taken into use
+ * @throws IllegalArgumentException
+ * if {@code selectionMode} is <code>null</code>
+ * @see SelectionModel
+ */
+ public SelectionModel setSelectionMode(final SelectionMode selectionMode)
+ throws IllegalArgumentException {
+ if (selectionMode == null) {
+ throw new IllegalArgumentException("selection mode may not be null");
+ }
+ final SelectionModel newSelectionModel = selectionMode.createModel();
+ setSelectionModel(newSelectionModel);
+ return newSelectionModel;
+ }
+
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ // keep this javadoc in sync with SelectionModel.isSelected
+ public boolean isSelected(Object itemId) {
+ return selectionModel.isSelected(itemId);
+ }
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ // keep this javadoc in sync with SelectionModel.getSelectedRows
+ public Collection<Object> getSelectedRows() {
+ return getSelectionModel().getSelectedRows();
+ }
+
+ /**
+ * Gets the item id of the currently selected item.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} is supported.
+ *
+ * @return the item id of the currently selected item, or <code>null</code>
+ * if nothing is selected
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.getSelectedRow
+ public Object getSelectedRow() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).getSelectedRow();
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ throw new IllegalStateException("Cannot get unique selected row: "
+ + "Grid is in multiselect mode "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot get selected row: "
+ + "Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot get selected row: "
+ + "Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as selected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to mark as selected
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might be that
+ * the implementation already had an item selected, and that
+ * needs to be explicitly deselected before re-selecting
+ * something.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {@code SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.select
+ public boolean select(Object itemId) throws IllegalArgumentException,
+ IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot select row '" + itemId
+ + "': Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot select row '" + itemId
+ + "': Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as unselected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to remove from being selected
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId was already selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation requires one or more items to be selected
+ * at all times.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {code SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.deselect
+ public boolean deselect(Object itemId) throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ if (isSelected(itemId)) {
+ return ((SelectionModel.Single) selectionModel).select(null);
+ }
+ return false;
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselect(itemId);
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot deselect row '" + itemId
+ + "': Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot deselect row '" + itemId
+ + "': Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks all items as unselected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId was already selected
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation requires one or more items to be selected
+ * at all times.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {code SelectionModel.Multi}
+ */
+ public boolean deselectAll() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ if (getSelectedRow() != null) {
+ return deselect(getSelectedRow());
+ }
+ return false;
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselectAll();
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot deselect all rows"
+ + ": Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot deselect all rows:"
+ + " Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Fires a selection change event.
+ * <p>
+ * <strong>Note:</strong> This is not a method that should be called by
+ * application logic. This method is publicly accessible only so that
+ * {@link SelectionModel SelectionModels} would be able to inform Grid of
+ * these events.
+ *
+ * @param newSelection
+ * the selection that was added by this event
+ * @param oldSelection
+ * the selection that was removed by this event
+ */
+ public void fireSelectionEvent(Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ fireEvent(new SelectionEvent(this, oldSelection, newSelection));
+ }
+
+ @Override
+ public void addSelectionListener(SelectionListener listener) {
+ addListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeSelectionListener(SelectionListener listener) {
+ removeListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
+ }
+
+ private void fireColumnReorderEvent(boolean userOriginated) {
+ fireEvent(new ColumnReorderEvent(this, userOriginated));
+ }
+
+ /**
+ * Registers a new column reorder listener.
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnReorderListener(ColumnReorderListener listener) {
+ addListener(ColumnReorderEvent.class, listener, COLUMN_REORDER_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column reorder listener.
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnReorderListener(ColumnReorderListener listener) {
+ removeListener(ColumnReorderEvent.class, listener,
+ COLUMN_REORDER_METHOD);
+ }
+
+ private void fireColumnResizeEvent(Column column, boolean userOriginated) {
+ fireEvent(new ColumnResizeEvent(this, column, userOriginated));
+ }
+
+ /**
+ * Registers a new column resize listener.
+ *
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnResizeListener(ColumnResizeListener listener) {
+ addListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column resize listener.
+ *
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnResizeListener(ColumnResizeListener listener) {
+ removeListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD);
+ }
+
+ /**
+ * Gets the {@link KeyMapper } being used by the data source.
+ *
+ * @return the key mapper being used by the data source
+ */
+ KeyMapper<Object> getKeyMapper() {
+ return datasourceExtension.getKeyMapper();
+ }
+
+ /**
+ * Adds a renderer to this grid's connector hierarchy.
+ *
+ * @param renderer
+ * the renderer to add
+ */
+ void addRenderer(Renderer<?> renderer) {
+ addExtension(renderer);
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param s
+ * a sort instance
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sort this Grid in ascending order by a specified property.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param propertyId
+ * a property ID
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Object propertyId) {
+ sort(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sort this Grid in user-specified {@link SortOrder} by a property.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param propertyId
+ * a property ID
+ * @param direction
+ * a sort order value (ascending/descending)
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Object propertyId, SortDirection direction) {
+ sort(Sort.by(propertyId, direction));
+ }
+
+ /**
+ * Clear the current sort order, and re-sort the grid.
+ */
+ public void clearSortOrder() {
+ sortOrder.clear();
+ sort(false);
+ }
+
+ /**
+ * Sets the sort order to use.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param order
+ * a sort order list.
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if order is null or trying to sort by non-existing property
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, false);
+ }
+
+ private void setSortOrder(List<SortOrder> order, boolean userOriginated)
+ throws IllegalStateException, IllegalArgumentException {
+ if (!(getContainerDataSource() instanceof Container.Sortable)) {
+ throw new IllegalStateException(
+ "Attached container is not sortable (does not implement Container.Sortable)");
+ }
+
+ if (order == null) {
+ throw new IllegalArgumentException("Order list may not be null!");
+ }
+
+ sortOrder.clear();
+
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
+ .getSortableContainerPropertyIds();
+
+ for (SortOrder o : order) {
+ if (!sortableProps.contains(o.getPropertyId())) {
+ throw new IllegalArgumentException(
+ "Property "
+ + o.getPropertyId()
+ + " does not exist or is not sortable in the current container");
+ }
+ }
+
+ sortOrder.addAll(order);
+ sort(userOriginated);
+ }
+
+ /**
+ * Get the current sort order list.
+ *
+ * @return a sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(boolean userOriginated) {
+
+ Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable) {
+ Container.Sortable cs = (Container.Sortable) c;
+
+ final int items = sortOrder.size();
+ Object[] propertyIds = new Object[items];
+ boolean[] directions = new boolean[items];
+
+ SortDirection[] stateDirs = new SortDirection[items];
+
+ for (int i = 0; i < items; ++i) {
+ SortOrder order = sortOrder.get(i);
+
+ stateDirs[i] = order.getDirection();
+ propertyIds[i] = order.getPropertyId();
+ switch (order.getDirection()) {
+ case ASCENDING:
+ directions[i] = true;
+ break;
+ case DESCENDING:
+ directions[i] = false;
+ break;
+ default:
+ throw new IllegalArgumentException("getDirection() of "
+ + order + " returned an unexpected value");
+ }
+ }
+
+ cs.sort(propertyIds, directions);
+
+ if (columns.keySet().containsAll(Arrays.asList(propertyIds))) {
+ String[] columnKeys = new String[items];
+ for (int i = 0; i < items; ++i) {
+ columnKeys[i] = this.columnKeys.key(propertyIds[i]);
+ }
+ getState().sortColumns = columnKeys;
+ getState(false).sortDirs = stateDirs;
+ } else {
+ // Not all sorted properties are in Grid. Remove any indicators.
+ getState().sortColumns = new String[] {};
+ getState(false).sortDirs = new SortDirection[] {};
+ }
+ fireEvent(new SortEvent(this, new ArrayList<SortOrder>(sortOrder),
+ userOriginated));
+ } else {
+ throw new IllegalStateException(
+ "Container is not sortable (does not implement Container.Sortable)");
+ }
+ }
+
+ /**
+ * Adds a sort order change listener that gets notified when the sort order
+ * changes.
+ *
+ * @param listener
+ * the sort order change listener to add
+ */
+ @Override
+ public void addSortListener(SortListener listener) {
+ addListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes a sort order change listener previously added using
+ * {@link #addSortListener(SortListener)}.
+ *
+ * @param listener
+ * the sort order change listener to remove
+ */
+ @Override
+ public void removeSortListener(SortListener listener) {
+ removeListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /* Grid Headers */
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ protected Header getHeader() {
+ return header;
+ }
+
+ /**
+ * Gets the header row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return header row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public HeaderRow getHeaderRow(int rowIndex) {
+ return header.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the header section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow addHeaderRowAt(int index) {
+ return header.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the header section.
+ *
+ * @return the new row
+ * @see #prependHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow appendHeaderRow() {
+ return header.appendRow();
+ }
+
+ /**
+ * Returns the current default row of the header section. The default row is
+ * a special header row providing a user interface for sorting columns.
+ * Setting a header text for column updates cells in the default header.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultHeaderRow() {
+ return header.getDefaultRow();
+ }
+
+ /**
+ * Gets the row count for the header section.
+ *
+ * @return row count
+ */
+ public int getHeaderRowCount() {
+ return header.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the header section.
+ *
+ * @return the new row
+ * @see #appendHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow prependHeaderRow() {
+ return header.prependRow();
+ }
+
+ /**
+ * Removes the given row from the header section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeHeaderRow(int)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(HeaderRow row) {
+ header.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the header section.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(int rowIndex) {
+ header.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the default row of the header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * header does not contain the row
+ */
+ public void setDefaultHeaderRow(HeaderRow row) {
+ header.setDefaultRow(row);
+ }
+
+ /**
+ * Sets the visibility of the header section.
+ *
+ * @param visible
+ * true to show header section, false to hide
+ */
+ public void setHeaderVisible(boolean visible) {
+ header.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the header section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isHeaderVisible() {
+ return header.isVisible();
+ }
+
+ /* Grid Footers */
+
+ /**
+ * Returns the footer section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the footer
+ */
+ protected Footer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Gets the footer row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return footer row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public FooterRow getFooterRow(int rowIndex) {
+ return footer.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the footer section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow addFooterRowAt(int index) {
+ return footer.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the footer section.
+ *
+ * @return the new row
+ * @see #prependFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow appendFooterRow() {
+ return footer.appendRow();
+ }
+
+ /**
+ * Gets the row count for the footer.
+ *
+ * @return row count
+ */
+ public int getFooterRowCount() {
+ return footer.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the footer section.
+ *
+ * @return the new row
+ * @see #appendFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow prependFooterRow() {
+ return footer.prependRow();
+ }
+
+ /**
+ * Removes the given row from the footer section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeFooterRow(int)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(FooterRow row) {
+ footer.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the footer section.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeFooterRow(FooterRow)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(int rowIndex) {
+ footer.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the visibility of the footer section.
+ *
+ * @param visible
+ * true to show footer section, false to hide
+ */
+ public void setFooterVisible(boolean visible) {
+ footer.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the footer section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isFooterVisible() {
+ return footer.isVisible();
+ }
+
+ private void addComponent(Component c) {
+ extensionComponents.add(c);
+ c.setParent(this);
+ markAsDirty();
+ }
+
+ private void removeComponent(Component c) {
+ extensionComponents.remove(c);
+ c.setParent(null);
+ markAsDirty();
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ // This is a hash set to avoid adding header/footer components inside
+ // merged cells multiple times
+ LinkedHashSet<Component> componentList = new LinkedHashSet<Component>();
+
+ Header header = getHeader();
+ for (int i = 0; i < header.getRowCount(); ++i) {
+ HeaderRow row = header.getRow(i);
+ for (Object propId : columns.keySet()) {
+ HeaderCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ Footer footer = getFooter();
+ for (int i = 0; i < footer.getRowCount(); ++i) {
+ FooterRow row = footer.getRow(i);
+ for (Object propId : columns.keySet()) {
+ FooterCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ componentList.addAll(getEditorFields());
+
+ componentList.addAll(extensionComponents);
+
+ return componentList.iterator();
+ }
+
+ @Override
+ public boolean isRendered(Component childComponent) {
+ if (getEditorFields().contains(childComponent)) {
+ // Only render editor fields if the editor is open
+ return isEditorActive();
+ } else {
+ // TODO Header and footer components should also only be rendered if
+ // the header/footer is visible
+ return true;
+ }
+ }
+
+ EditorClientRpc getEditorRpc() {
+ return getRpcProxy(EditorClientRpc.class);
+ }
+
+ /**
+ * Sets the {@code CellDescriptionGenerator} instance for generating
+ * optional descriptions (tooltips) for individual Grid cells. If a
+ * {@link RowDescriptionGenerator} is also set, the row description it
+ * generates is displayed for cells for which {@code generator} returns
+ * null.
+ *
+ * @param generator
+ * the description generator to use or {@code null} to remove a
+ * previously set generator if any
+ *
+ * @see #setRowDescriptionGenerator(RowDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public void setCellDescriptionGenerator(CellDescriptionGenerator generator) {
+ cellDescriptionGenerator = generator;
+ getState().hasDescriptions = (generator != null || rowDescriptionGenerator != null);
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Returns the {@code CellDescriptionGenerator} instance used to generate
+ * descriptions (tooltips) for Grid cells.
+ *
+ * @return the description generator or {@code null} if no generator is set
+ *
+ * @since 7.6
+ */
+ public CellDescriptionGenerator getCellDescriptionGenerator() {
+ return cellDescriptionGenerator;
+ }
+
+ /**
+ * Sets the {@code RowDescriptionGenerator} instance for generating optional
+ * descriptions (tooltips) for Grid rows. If a
+ * {@link CellDescriptionGenerator} is also set, the row description
+ * generated by {@code generator} is used for cells for which the cell
+ * description generator returns null.
+ *
+ *
+ * @param generator
+ * the description generator to use or {@code null} to remove a
+ * previously set generator if any
+ *
+ * @see #setCellDescriptionGenerator(CellDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public void setRowDescriptionGenerator(RowDescriptionGenerator generator) {
+ rowDescriptionGenerator = generator;
+ getState().hasDescriptions = (generator != null || cellDescriptionGenerator != null);
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Returns the {@code RowDescriptionGenerator} instance used to generate
+ * descriptions (tooltips) for Grid rows
+ *
+ * @return the description generator or {@code} null if no generator is set
+ *
+ * @since 7.6
+ */
+ public RowDescriptionGenerator getRowDescriptionGenerator() {
+ return rowDescriptionGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for cells
+ *
+ * @param cellStyleGenerator
+ * the cell style generator to set, or <code>null</code> to
+ * remove a previously set generator
+ */
+ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for cells
+ *
+ * @return the cell style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public CellStyleGenerator getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for rows
+ *
+ * @param rowStyleGenerator
+ * the row style generator to set, or <code>null</code> to remove
+ * a previously set generator
+ */
+ public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) {
+ this.rowStyleGenerator = rowStyleGenerator;
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for rows
+ *
+ * @return the row style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public RowStyleGenerator getRowStyleGenerator() {
+ return rowStyleGenerator;
+ }
+
+ /**
+ * Adds a row to the underlying container. The order of the parameters
+ * should match the current visible column order.
+ * <p>
+ * Please note that it's generally only safe to use this method during
+ * initialization. After Grid has been initialized and the visible column
+ * order might have been changed, it's better to instead add items directly
+ * to the underlying container and use {@link Item#getItemProperty(Object)}
+ * to make sure each value is assigned to the intended property.
+ *
+ * @param values
+ * the cell values of the new row, in the same order as the
+ * visible column order, not <code>null</code>.
+ * @return the item id of the new row
+ * @throws IllegalArgumentException
+ * if values is null
+ * @throws IllegalArgumentException
+ * if its length does not match the number of visible columns
+ * @throws IllegalArgumentException
+ * if a parameter value is not an instance of the corresponding
+ * property type
+ * @throws UnsupportedOperationException
+ * if the container does not support adding new items
+ */
+ public Object addRow(Object... values) {
+ if (values == null) {
+ throw new IllegalArgumentException("Values cannot be null");
+ }
+
+ Indexed dataSource = getContainerDataSource();
+ List<String> columnOrder = getState(false).columnOrder;
+
+ if (values.length != columnOrder.size()) {
+ throw new IllegalArgumentException("There are "
+ + columnOrder.size() + " visible columns, but "
+ + values.length + " cell values were provided.");
+ }
+
+ // First verify all parameter types
+ for (int i = 0; i < columnOrder.size(); i++) {
+ Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
+
+ Class<?> propertyType = dataSource.getType(propertyId);
+ if (values[i] != null && !propertyType.isInstance(values[i])) {
+ throw new IllegalArgumentException("Parameter " + i + "("
+ + values[i] + ") is not an instance of "
+ + propertyType.getCanonicalName());
+ }
+ }
+
+ Object itemId = dataSource.addItem();
+ try {
+ Item item = dataSource.getItem(itemId);
+ for (int i = 0; i < columnOrder.size(); i++) {
+ Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
+ Property<Object> property = item.getItemProperty(propertyId);
+ property.setValue(values[i]);
+ }
+ } catch (RuntimeException e) {
+ try {
+ dataSource.removeItem(itemId);
+ } catch (Exception e2) {
+ getLogger().log(Level.SEVERE,
+ "Error recovering from exception in addRow", e);
+ }
+ throw e;
+ }
+
+ return itemId;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(Grid.class.getName());
+ }
+
+ /**
+ * Sets whether or not the item editor UI is enabled for this grid. When the
+ * editor is enabled, the user can open it by double-clicking a row or
+ * hitting enter when a row is focused. The editor can also be opened
+ * programmatically using the {@link #editItem(Object)} method.
+ *
+ * @param isEnabled
+ * <code>true</code> to enable the feature, <code>false</code>
+ * otherwise
+ * @throws IllegalStateException
+ * if an item is currently being edited
+ *
+ * @see #getEditedItemId()
+ */
+ public void setEditorEnabled(boolean isEnabled)
+ throws IllegalStateException {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot disable the editor while an item ("
+ + getEditedItemId() + ") is being edited");
+ }
+ if (isEditorEnabled() != isEnabled) {
+ getState().editorEnabled = isEnabled;
+ }
+ }
+
+ /**
+ * Checks whether the item editor UI is enabled for this grid.
+ *
+ * @return <code>true</code> iff the editor is enabled for this grid
+ *
+ * @see #setEditorEnabled(boolean)
+ * @see #getEditedItemId()
+ */
+ public boolean isEditorEnabled() {
+ return getState(false).editorEnabled;
+ }
+
+ /**
+ * Gets the id of the item that is currently being edited.
+ *
+ * @return the id of the item that is currently being edited, or
+ * <code>null</code> if no item is being edited at the moment
+ */
+ public Object getEditedItemId() {
+ return editedItemId;
+ }
+
+ /**
+ * Gets the field group that is backing the item editor of this grid.
+ *
+ * @return the backing field group
+ */
+ public FieldGroup getEditorFieldGroup() {
+ return editorFieldGroup;
+ }
+
+ /**
+ * Sets the field group that is backing the item editor of this grid.
+ *
+ * @param fieldGroup
+ * the backing field group
+ *
+ * @throws IllegalStateException
+ * if the editor is currently active
+ */
+ public void setEditorFieldGroup(FieldGroup fieldGroup) {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot change field group while an item ("
+ + getEditedItemId() + ") is being edited");
+ }
+ editorFieldGroup = fieldGroup;
+ }
+
+ /**
+ * Returns whether an item is currently being edited in the editor.
+ *
+ * @return true iff the editor is open
+ */
+ public boolean isEditorActive() {
+ return editorActive;
+ }
+
+ private void checkColumnExists(Object propertyId) {
+ if (getColumn(propertyId) == null) {
+ throw new IllegalArgumentException(
+ "There is no column with the property id " + propertyId);
+ }
+ }
+
+ private Field<?> getEditorField(Object propertyId) {
+ checkColumnExists(propertyId);
+
+ if (!getColumn(propertyId).isEditable()) {
+ return null;
+ }
+
+ Field<?> editor = editorFieldGroup.getField(propertyId);
+
+ try {
+ if (editor == null) {
+ editor = editorFieldGroup.buildAndBind(propertyId);
+ }
+ } finally {
+ if (editor == null) {
+ editor = editorFieldGroup.getField(propertyId);
+ }
+
+ if (editor != null && editor.getParent() != Grid.this) {
+ assert editor.getParent() == null;
+ editor.setParent(this);
+ }
+ }
+ return editor;
+ }
+
+ /**
+ * Opens the editor interface for the provided item. Scrolls the Grid to
+ * bring the item to view if it is not already visible.
+ *
+ * Note that any cell content rendered by a WidgetRenderer will not be
+ * visible in the editor row.
+ *
+ * @param itemId
+ * the id of the item to edit
+ * @throws IllegalStateException
+ * if the editor is not enabled or already editing an item in
+ * buffered mode
+ * @throws IllegalArgumentException
+ * if the {@code itemId} is not in the backing container
+ * @see #setEditorEnabled(boolean)
+ */
+ public void editItem(Object itemId) throws IllegalStateException,
+ IllegalArgumentException {
+ if (!isEditorEnabled()) {
+ throw new IllegalStateException("Item editor is not enabled");
+ } else if (isEditorBuffered() && editedItemId != null) {
+ throw new IllegalStateException("Editing item " + itemId
+ + " failed. Item editor is already editing item "
+ + editedItemId);
+ } else if (!getContainerDataSource().containsId(itemId)) {
+ throw new IllegalArgumentException("Item with id " + itemId
+ + " not found in current container");
+ }
+ editedItemId = itemId;
+ getEditorRpc().bind(getContainerDataSource().indexOfId(itemId));
+ }
+
+ protected void doEditItem() {
+ Item item = getContainerDataSource().getItem(editedItemId);
+
+ editorFieldGroup.setItemDataSource(item);
+
+ for (Column column : getColumns()) {
+ column.getState().editorConnector = getEditorField(column
+ .getPropertyId());
+ }
+
+ editorActive = true;
+ // Must ensure that all fields, recursively, are sent to the client
+ // This is needed because the fields are hidden using isRendered
+ for (Field<?> f : getEditorFields()) {
+ f.markAsDirtyRecursive();
+ }
+
+ if (datasource instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) datasource)
+ .addItemSetChangeListener(editorClosingItemSetListener);
+ }
+ }
+
+ private void setEditorField(Object propertyId, Field<?> field) {
+ checkColumnExists(propertyId);
+
+ Field<?> oldField = editorFieldGroup.getField(propertyId);
+ if (oldField != null) {
+ editorFieldGroup.unbind(oldField);
+ oldField.setParent(null);
+ }
+
+ if (field != null) {
+ field.setParent(this);
+ editorFieldGroup.bind(field, propertyId);
+ }
+ }
+
+ /**
+ * Saves all changes done to the bound fields.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @throws CommitException
+ * If the commit was aborted
+ *
+ * @see FieldGroup#commit()
+ */
+ public void saveEditor() throws CommitException {
+ editorFieldGroup.commit();
+ }
+
+ /**
+ * Cancels the currently active edit if any. Hides the editor and discards
+ * possible unsaved changes in the editor fields.
+ */
+ public void cancelEditor() {
+ if (isEditorActive()) {
+ getEditorRpc().cancel(
+ getContainerDataSource().indexOfId(editedItemId));
+ doCancelEditor();
+ }
+ }
+
+ protected void doCancelEditor() {
+ editedItemId = null;
+ editorActive = false;
+ editorFieldGroup.discard();
+ editorFieldGroup.setItemDataSource(null);
+
+ if (datasource instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) datasource)
+ .removeItemSetChangeListener(editorClosingItemSetListener);
+ }
+
+ // Mark Grid as dirty so the client side gets to know that the editors
+ // are no longer attached
+ markAsDirty();
+ }
+
+ void resetEditor() {
+ if (isEditorActive()) {
+ /*
+ * Simply force cancel the editing; throwing here would just make
+ * Grid.setContainerDataSource semantics more complicated.
+ */
+ cancelEditor();
+ }
+ for (Field<?> editor : getEditorFields()) {
+ editor.setParent(null);
+ }
+
+ editedItemId = null;
+ editorActive = false;
+ editorFieldGroup = new CustomFieldGroup();
+ }
+
+ /**
+ * Gets a collection of all fields bound to the item editor of this grid.
+ * <p>
+ * When {@link #editItem(Object) editItem} is called, fields are
+ * automatically created and bound to any unbound properties.
+ *
+ * @return a collection of all the fields bound to the item editor
+ */
+ Collection<Field<?>> getEditorFields() {
+ Collection<Field<?>> fields = editorFieldGroup.getFields();
+ assert allAttached(fields);
+ return fields;
+ }
+
+ private boolean allAttached(Collection<? extends Component> components) {
+ for (Component component : components) {
+ if (component.getParent() != this) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @param fieldFactory
+ * The field factory to use
+ */
+ public void setEditorFieldFactory(FieldGroupFieldFactory fieldFactory) {
+ editorFieldGroup.setFieldFactory(fieldFactory);
+ }
+
+ /**
+ * Sets the error handler for the editor.
+ *
+ * The error handler is called whenever there is an exception in the editor.
+ *
+ * @param editorErrorHandler
+ * The editor error handler to use
+ * @throws IllegalArgumentException
+ * if the error handler is null
+ */
+ public void setEditorErrorHandler(EditorErrorHandler editorErrorHandler)
+ throws IllegalArgumentException {
+ if (editorErrorHandler == null) {
+ throw new IllegalArgumentException(
+ "The error handler cannot be null");
+ }
+ this.editorErrorHandler = editorErrorHandler;
+ }
+
+ /**
+ * Gets the error handler used for the editor
+ *
+ * @see #setErrorHandler(com.vaadin.server.ErrorHandler)
+ * @return the editor error handler, never null
+ */
+ public EditorErrorHandler getEditorErrorHandler() {
+ return editorErrorHandler;
+ }
+
+ /**
+ * Gets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @return The field factory in use
+ */
+ public FieldGroupFieldFactory getEditorFieldFactory() {
+ return editorFieldGroup.getFieldFactory();
+ }
+
+ /**
+ * Sets the caption on the save button in the Grid editor.
+ *
+ * @param saveCaption
+ * the caption to set
+ * @throws IllegalArgumentException
+ * if {@code saveCaption} is {@code null}
+ */
+ public void setEditorSaveCaption(String saveCaption)
+ throws IllegalArgumentException {
+ if (saveCaption == null) {
+ throw new IllegalArgumentException("Save caption cannot be null");
+ }
+ getState().editorSaveCaption = saveCaption;
+ }
+
+ /**
+ * Gets the current caption of the save button in the Grid editor.
+ *
+ * @return the current caption of the save button
+ */
+ public String getEditorSaveCaption() {
+ return getState(false).editorSaveCaption;
+ }
+
+ /**
+ * Sets the caption on the cancel button in the Grid editor.
+ *
+ * @param cancelCaption
+ * the caption to set
+ * @throws IllegalArgumentException
+ * if {@code cancelCaption} is {@code null}
+ */
+ public void setEditorCancelCaption(String cancelCaption)
+ throws IllegalArgumentException {
+ if (cancelCaption == null) {
+ throw new IllegalArgumentException("Cancel caption cannot be null");
+ }
+ getState().editorCancelCaption = cancelCaption;
+ }
+
+ /**
+ * Gets the current caption of the cancel button in the Grid editor.
+ *
+ * @return the current caption of the cancel button
+ */
+ public String getEditorCancelCaption() {
+ return getState(false).editorCancelCaption;
+ }
+
+ /**
+ * Sets the buffered editor mode. The default mode is buffered (
+ * <code>true</code>).
+ *
+ * @since 7.6
+ * @param editorBuffered
+ * <code>true</code> to enable buffered editor,
+ * <code>false</code> to disable it
+ * @throws IllegalStateException
+ * If editor is active while attempting to change the buffered
+ * mode.
+ */
+ public void setEditorBuffered(boolean editorBuffered)
+ throws IllegalStateException {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Can't change editor unbuffered mode while editor is active.");
+ }
+ getState().editorBuffered = editorBuffered;
+ editorFieldGroup.setBuffered(editorBuffered);
+ }
+
+ /**
+ * Gets the buffered editor mode.
+ *
+ * @since 7.6
+ * @return <code>true</code> if buffered editor is enabled,
+ * <code>false</code> otherwise
+ */
+ public boolean isEditorBuffered() {
+ return getState(false).editorBuffered;
+ }
+
+ @Override
+ public void addItemClickListener(ItemClickListener listener) {
+ addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener, ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ @Override
+ @Deprecated
+ public void addListener(ItemClickListener listener) {
+ addItemClickListener(listener);
+ }
+
+ @Override
+ public void removeItemClickListener(ItemClickListener listener) {
+ removeListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener);
+ }
+
+ @Override
+ @Deprecated
+ public void removeListener(ItemClickListener listener) {
+ removeItemClickListener(listener);
+ }
+
+ /**
+ * Requests that the column widths should be recalculated.
+ * <p>
+ * In most cases Grid will know when column widths need to be recalculated
+ * but this method can be used to force recalculation in situations when
+ * grid does not recalculate automatically.
+ *
+ * @since 7.4.1
+ */
+ public void recalculateColumnWidths() {
+ getRpcProxy(GridClientRpc.class).recalculateColumnWidths();
+ }
+
+ /**
+ * Registers a new column visibility change listener
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnVisibilityChangeListener(
+ ColumnVisibilityChangeListener listener) {
+ addListener(ColumnVisibilityChangeEvent.class, listener,
+ COLUMN_VISIBILITY_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column visibility change listener
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnVisibilityChangeListener(
+ ColumnVisibilityChangeListener listener) {
+ removeListener(ColumnVisibilityChangeEvent.class, listener,
+ COLUMN_VISIBILITY_METHOD);
+ }
+
+ private void fireColumnVisibilityChangeEvent(Column column, boolean hidden,
+ boolean isUserOriginated) {
+ fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden,
+ isUserOriginated));
+ }
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @since 7.5.0
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+ detailComponentManager.setDetailsGenerator(detailsGenerator);
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @since 7.5.0
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailComponentManager.getDetailsGenerator();
+ }
+
+ /**
+ * Shows or hides the details for a specific item.
+ *
+ * @since 7.5.0
+ * @param itemId
+ * the id of the item for which to set details visibility
+ * @param visible
+ * <code>true</code> to show the details, or <code>false</code>
+ * to hide them
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ detailComponentManager.setDetailsVisible(itemId, visible);
+ }
+
+ /**
+ * Checks whether details are visible for the given item.
+ *
+ * @since 7.5.0
+ * @param itemId
+ * the id of the item for which to check details visibility
+ * @return <code>true</code> iff the details are visible
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return detailComponentManager.isDetailsVisible(itemId);
+ }
+
+ private static SelectionMode getDefaultSelectionMode() {
+ return SelectionMode.SINGLE;
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext context) {
+ super.readDesign(design, context);
+
+ Attributes attrs = design.attributes();
+ if (attrs.hasKey("editable")) {
+ setEditorEnabled(DesignAttributeHandler.readAttribute("editable",
+ attrs, boolean.class));
+ }
+ if (attrs.hasKey("rows")) {
+ setHeightByRows(DesignAttributeHandler.readAttribute("rows", attrs,
+ double.class));
+ setHeightMode(HeightMode.ROW);
+ }
+ if (attrs.hasKey("selection-mode")) {
+ setSelectionMode(DesignAttributeHandler.readAttribute(
+ "selection-mode", attrs, SelectionMode.class));
+ }
+
+ if (design.children().size() > 0) {
+ if (design.children().size() > 1
+ || !design.child(0).tagName().equals("table")) {
+ throw new DesignException(
+ "Grid needs to have a table element as its only child");
+ }
+ Element table = design.child(0);
+
+ Elements colgroups = table.getElementsByTag("colgroup");
+ if (colgroups.size() != 1) {
+ throw new DesignException(
+ "Table element in declarative Grid needs to have a"
+ + " colgroup defining the columns used in Grid");
+ }
+
+ int i = 0;
+ for (Element col : colgroups.get(0).getElementsByTag("col")) {
+ String propertyId = DesignAttributeHandler.readAttribute(
+ "property-id", col.attributes(), "property-" + i,
+ String.class);
+ addColumn(propertyId, String.class).readDesign(col, context);
+ ++i;
+ }
+
+ for (Element child : table.children()) {
+ if (child.tagName().equals("thead")) {
+ header.readDesign(child, context);
+ } else if (child.tagName().equals("tbody")) {
+ for (Element row : child.children()) {
+ Elements cells = row.children();
+ Object[] data = new String[cells.size()];
+ for (int c = 0; c < cells.size(); ++c) {
+ data[c] = cells.get(c).html();
+ }
+ addRow(data);
+ }
+
+ // Since inline data is used, set HTML renderer for columns
+ for (Column c : getColumns()) {
+ c.setRenderer(new HtmlRenderer());
+ }
+ } else if (child.tagName().equals("tfoot")) {
+ footer.readDesign(child, context);
+ }
+ }
+ }
+
+ // Read frozen columns after columns are read.
+ if (attrs.hasKey("frozen-columns")) {
+ setFrozenColumnCount(DesignAttributeHandler.readAttribute(
+ "frozen-columns", attrs, int.class));
+ }
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext context) {
+ super.writeDesign(design, context);
+
+ Attributes attrs = design.attributes();
+ Grid def = context.getDefaultInstance(this);
+
+ DesignAttributeHandler.writeAttribute("editable", attrs,
+ isEditorEnabled(), def.isEditorEnabled(), boolean.class);
+
+ DesignAttributeHandler.writeAttribute("frozen-columns", attrs,
+ getFrozenColumnCount(), def.getFrozenColumnCount(), int.class);
+
+ if (getHeightMode() == HeightMode.ROW) {
+ DesignAttributeHandler.writeAttribute("rows", attrs,
+ getHeightByRows(), def.getHeightByRows(), double.class);
+ }
+
+ SelectionMode selectionMode = null;
+
+ if (selectionModel.getClass().equals(SingleSelectionModel.class)) {
+ selectionMode = SelectionMode.SINGLE;
+ } else if (selectionModel.getClass().equals(MultiSelectionModel.class)) {
+ selectionMode = SelectionMode.MULTI;
+ } else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
+ selectionMode = SelectionMode.NONE;
+ }
+
+ assert selectionMode != null : "Unexpected selection model "
+ + selectionModel.getClass().getName();
+
+ DesignAttributeHandler.writeAttribute("selection-mode", attrs,
+ selectionMode, getDefaultSelectionMode(), SelectionMode.class);
+
+ if (columns.isEmpty()) {
+ // Empty grid. Structure not needed.
+ return;
+ }
+
+ // Do structure.
+ Element tableElement = design.appendElement("table");
+ Element colGroup = tableElement.appendElement("colgroup");
+
+ List<Column> columnOrder = getColumns();
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ Column column = columnOrder.get(i);
+ Element colElement = colGroup.appendElement("col");
+ column.writeDesign(colElement, context);
+ }
+
+ // Always write thead. Reads correctly when there no header rows
+ header.writeDesign(tableElement.appendElement("thead"), context);
+
+ if (context.shouldWriteData(this)) {
+ Element bodyElement = tableElement.appendElement("tbody");
+ for (Object itemId : datasource.getItemIds()) {
+ Element tableRow = bodyElement.appendElement("tr");
+ for (Column c : getColumns()) {
+ Object value = datasource.getItem(itemId)
+ .getItemProperty(c.getPropertyId()).getValue();
+ tableRow.appendElement("td").append(
+ (value != null ? DesignFormatter
+ .encodeForTextNode(value.toString()) : ""));
+ }
+ }
+ }
+
+ if (footer.getRowCount() > 0) {
+ footer.writeDesign(tableElement.appendElement("tfoot"), context);
+ }
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add("editor-enabled");
+ result.add("editable");
+ result.add("frozen-column-count");
+ result.add("frozen-columns");
+ result.add("height-by-rows");
+ result.add("rows");
+ result.add("selection-mode");
+ result.add("header-visible");
+ result.add("footer-visible");
+ result.add("editor-error-handler");
+ result.add("height-mode");
+
+ return result;
+ }
+}
--- /dev/null
-
+/*
+ * Copyright 2000-2014 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.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+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.Map.Entry;
+import java.util.Set;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import com.vaadin.event.LayoutEvents.LayoutClickEvent;
+import com.vaadin.event.LayoutEvents.LayoutClickListener;
+import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.EventId;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.MarginInfo;
+import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc;
+import com.vaadin.shared.ui.gridlayout.GridLayoutState;
+import com.vaadin.shared.ui.gridlayout.GridLayoutState.ChildComponentData;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+
+/**
+ * A layout where the components are laid out on a grid using cell coordinates.
+ *
+ * <p>
+ * The GridLayout also maintains a cursor for adding components in
+ * left-to-right, top-to-bottom order.
+ * </p>
+ *
+ * <p>
+ * Each component in a <code>GridLayout</code> uses a defined
+ * {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. The
+ * components may not overlap with the existing components - if you try to do so
+ * you will get an {@link OverlapsException}. Adding a component with cursor
+ * automatically extends the grid by increasing the grid height.
+ * </p>
+ *
+ * <p>
+ * The grid coordinates, which are specified by a row and column index, always
+ * start from 0 for the topmost row and the leftmost column.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class GridLayout extends AbstractLayout implements
+ Layout.AlignmentHandler, Layout.SpacingHandler, Layout.MarginHandler,
+ LayoutClickNotifier, LegacyComponent {
+
+ private GridLayoutServerRpc rpc = new GridLayoutServerRpc() {
+
+ @Override
+ public void layoutClick(MouseEventDetails mouseDetails,
+ Connector clickedConnector) {
+ fireEvent(LayoutClickEvent.createEvent(GridLayout.this,
+ mouseDetails, clickedConnector));
+
+ }
+ };
+ /**
+ * Cursor X position: this is where the next component with unspecified x,y
+ * is inserted
+ */
+ private int cursorX = 0;
+
+ /**
+ * Cursor Y position: this is where the next component with unspecified x,y
+ * is inserted
+ */
+ private int cursorY = 0;
+
+ private final LinkedList<Component> components = new LinkedList<Component>();
+
+ private Map<Integer, Float> columnExpandRatio = new HashMap<Integer, Float>();
+ private Map<Integer, Float> rowExpandRatio = new HashMap<Integer, Float>();
+ private Alignment defaultComponentAlignment = Alignment.TOP_LEFT;
+
+ /**
+ * Constructor for a grid of given size (number of columns and rows).
+ *
+ * The grid may grow or shrink later. Grid grows automatically if you add
+ * components outside its area.
+ *
+ * @param columns
+ * Number of columns in the grid.
+ * @param rows
+ * Number of rows in the grid.
+ */
+ public GridLayout(int columns, int rows) {
+ setColumns(columns);
+ setRows(rows);
+ registerRpc(rpc);
+ }
+
+ /**
+ * Constructs an empty (1x1) grid layout that is extended as needed.
+ */
+ public GridLayout() {
+ this(1, 1);
+ }
+
+ /**
+ * Constructs a GridLayout of given size (number of columns and rows) and
+ * adds the given components in order to the grid.
+ *
+ * @see #addComponents(Component...)
+ *
+ * @param columns
+ * Number of columns in the grid.
+ * @param rows
+ * Number of rows in the grid.
+ * @param children
+ * Components to add to the grid.
+ */
+ public GridLayout(int columns, int rows, Component... children) {
+ this(columns, rows);
+ addComponents(children);
+ }
+
+ @Override
+ protected GridLayoutState getState() {
+ return (GridLayoutState) super.getState();
+ }
+
+ @Override
+ protected GridLayoutState getState(boolean markAsDirty) {
+ return (GridLayoutState) super.getState(markAsDirty);
+ }
+
+ /**
+ * <p>
+ * Adds a component to the grid in the specified area. The area is defined
+ * by specifying the upper left corner (column1, row1) and the lower right
+ * corner (column2, row2) of the area. The coordinates are zero-based.
+ * </p>
+ *
+ * <p>
+ * If the area overlaps with any of the existing components already present
+ * in the grid, the operation will fail and an {@link OverlapsException} is
+ * thrown.
+ * </p>
+ *
+ * @param component
+ * the component to be added, not <code>null</code>.
+ * @param column1
+ * the column of the upper left corner of the area <code>c</code>
+ * is supposed to occupy. The leftmost column has index 0.
+ * @param row1
+ * the row of the upper left corner of the area <code>c</code> is
+ * supposed to occupy. The topmost row has index 0.
+ * @param column2
+ * the column of the lower right corner of the area
+ * <code>c</code> is supposed to occupy.
+ * @param row2
+ * the row of the lower right corner of the area <code>c</code>
+ * is supposed to occupy.
+ * @throws OverlapsException
+ * if the new component overlaps with any of the components
+ * already in the grid.
+ * @throws OutOfBoundsException
+ * if the cells are outside the grid area.
+ */
+ public void addComponent(Component component, int column1, int row1,
+ int column2, int row2) throws OverlapsException,
+ OutOfBoundsException {
+
+ if (component == null) {
+ throw new NullPointerException("Component must not be null");
+ }
+
+ // Checks that the component does not already exist in the container
+ if (components.contains(component)) {
+ throw new IllegalArgumentException(
+ "Component is already in the container");
+ }
+
+ // Creates the area
+ final Area area = new Area(component, column1, row1, column2, row2);
+
+ // Checks the validity of the coordinates
+ if (column2 < column1 || row2 < row1) {
+ throw new IllegalArgumentException(
+ "Illegal coordinates for the component");
+ }
+ if (column1 < 0 || row1 < 0 || column2 >= getColumns()
+ || row2 >= getRows()) {
+ throw new OutOfBoundsException(area);
+ }
+
+ // Checks that newItem does not overlap with existing items
+ checkExistingOverlaps(area);
+
+ // Inserts the component to right place at the list
+ // Respect top-down, left-right ordering
+ // component.setParent(this);
+ final Iterator<Component> i = components.iterator();
+ final Map<Connector, ChildComponentData> childDataMap = getState().childData;
+ int index = 0;
+ boolean done = false;
+ while (!done && i.hasNext()) {
+ final ChildComponentData existingArea = childDataMap.get(i.next());
+ if ((existingArea.row1 >= row1 && existingArea.column1 > column1)
+ || existingArea.row1 > row1) {
+ components.add(index, component);
+ done = true;
+ }
+ index++;
+ }
+ if (!done) {
+ components.addLast(component);
+ }
+
+ childDataMap.put(component, area.childData);
+
+ // Attempt to add to super
+ try {
+ super.addComponent(component);
+ } catch (IllegalArgumentException e) {
+ childDataMap.remove(component);
+ components.remove(component);
+ throw e;
+ }
+
+ // update cursor position, if it's within this area; use first position
+ // outside this area, even if it's occupied
+ if (cursorX >= column1 && cursorX <= column2 && cursorY >= row1
+ && cursorY <= row2) {
+ // cursor within area
+ cursorX = column2 + 1; // one right of area
+ if (cursorX >= getColumns()) {
+ // overflowed columns
+ cursorX = 0; // first col
+ // move one row down, or one row under the area
+ cursorY = (column1 == 0 ? row2 : row1) + 1;
+ } else {
+ cursorY = row1;
+ }
+ }
+ }
+
+ /**
+ * Tests if the given area overlaps with any of the items already on the
+ * grid.
+ *
+ * @param area
+ * the Area to be checked for overlapping.
+ * @throws OverlapsException
+ * if <code>area</code> overlaps with any existing area.
+ */
+ private void checkExistingOverlaps(Area area) throws OverlapsException {
+ for (Entry<Connector, ChildComponentData> entry : getState().childData
+ .entrySet()) {
+ if (componentsOverlap(entry.getValue(), area.childData)) {
+ // Component not added, overlaps with existing component
+ throw new OverlapsException(new Area(entry.getValue(),
+ (Component) entry.getKey()));
+ }
+ }
+ }
+
+ /**
+ * Adds the component to the grid in cells column1,row1 (NortWest corner of
+ * the area.) End coordinates (SouthEast corner of the area) are the same as
+ * column1,row1. The coordinates are zero-based. Component width and height
+ * is 1.
+ *
+ * @param component
+ * the component to be added, not <code>null</code>.
+ * @param column
+ * the column index, starting from 0.
+ * @param row
+ * the row index, starting from 0.
+ * @throws OverlapsException
+ * if the new component overlaps with any of the components
+ * already in the grid.
+ * @throws OutOfBoundsException
+ * if the cell is outside the grid area.
+ */
+ public void addComponent(Component component, int column, int row)
+ throws OverlapsException, OutOfBoundsException {
+ this.addComponent(component, column, row, column, row);
+ }
+
+ /**
+ * Forces the next component to be added at the beginning of the next line.
+ *
+ * <p>
+ * Sets the cursor column to 0 and increments the cursor row by one.
+ * </p>
+ *
+ * <p>
+ * By calling this function you can ensure that no more components are added
+ * right of the previous component.
+ * </p>
+ *
+ * @see #space()
+ */
+ public void newLine() {
+ cursorX = 0;
+ cursorY++;
+ }
+
+ /**
+ * Moves the cursor forward by one. If the cursor goes out of the right grid
+ * border, it is moved to the first column of the next row.
+ *
+ * @see #newLine()
+ */
+ public void space() {
+ cursorX++;
+ if (cursorX >= getColumns()) {
+ cursorX = 0;
+ cursorY++;
+ }
+ }
+
+ /**
+ * Adds the component into this container to the cursor position. If the
+ * cursor position is already occupied, the cursor is moved forwards to find
+ * free position. If the cursor goes out from the bottom of the grid, the
+ * grid is automatically extended.
+ *
+ * @param component
+ * the component to be added, not <code>null</code>.
+ */
+ @Override
+ public void addComponent(Component component) {
+ if (component == null) {
+ throw new IllegalArgumentException("Component must not be null");
+ }
+
+ // Finds first available place from the grid
+ Area area;
+ boolean done = false;
+ while (!done) {
+ try {
+ area = new Area(component, cursorX, cursorY, cursorX, cursorY);
+ checkExistingOverlaps(area);
+ done = true;
+ } catch (final OverlapsException e) {
+ space();
+ }
+ }
+
+ // Extends the grid if needed
+ if (cursorX >= getColumns()) {
+ setColumns(cursorX + 1);
+ }
+ if (cursorY >= getRows()) {
+ setRows(cursorY + 1);
+ }
+
+ addComponent(component, cursorX, cursorY);
+ }
+
+ /**
+ * Removes the specified component from the layout.
+ *
+ * @param component
+ * the component to be removed.
+ */
+ @Override
+ public void removeComponent(Component component) {
+
+ // Check that the component is contained in the container
+ if (component == null || !components.contains(component)) {
+ return;
+ }
+
+ getState().childData.remove(component);
+ components.remove(component);
+ super.removeComponent(component);
+ }
+
+ /**
+ * Removes the component specified by its cell coordinates.
+ *
+ * @param column
+ * the component's column, starting from 0.
+ * @param row
+ * the component's row, starting from 0.
+ */
+ public void removeComponent(int column, int row) {
+
+ // Finds the area
+ for (final Iterator<Component> i = components.iterator(); i.hasNext();) {
+ final Component component = i.next();
+ final ChildComponentData childData = getState().childData
+ .get(component);
+ if (childData.column1 == column && childData.row1 == row) {
+ removeComponent(component);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Gets an Iterator for the components contained in the layout. By using the
+ * Iterator it is possible to step through the contents of the layout.
+ *
+ * @return the Iterator of the components inside the layout.
+ */
+ @Override
+ public Iterator<Component> iterator() {
+ return Collections.unmodifiableCollection(components).iterator();
+ }
+
+ /**
+ * Gets the number of components contained in the layout. Consistent with
+ * the iterator returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components
+ */
+ @Override
+ public int getComponentCount() {
+ return components.size();
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // TODO Remove once LegacyComponent is no longer implemented
+ }
+
+ /**
+ * Paints the contents of this component.
+ *
+ * @param target
+ * the Paint Event.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ final Integer[] columnExpandRatioArray = new Integer[getColumns()];
+ final Integer[] rowExpandRatioArray = new Integer[getRows()];
+
+ int realColExpandRatioSum = 0;
+ float colSum = getExpandRatioSum(columnExpandRatio);
+ if (colSum == 0) {
+ // no columns has been expanded, all cols have same expand
+ // rate
+ float equalSize = 1 / (float) getColumns();
+ int myRatio = Math.round(equalSize * 1000);
+ for (int i = 0; i < getColumns(); i++) {
+ columnExpandRatioArray[i] = myRatio;
+ }
+ realColExpandRatioSum = myRatio * getColumns();
+ } else {
+ for (int i = 0; i < getColumns(); i++) {
+ int myRatio = Math
+ .round((getColumnExpandRatio(i) / colSum) * 1000);
+ columnExpandRatioArray[i] = myRatio;
+ realColExpandRatioSum += myRatio;
+ }
+ }
+
+ int realRowExpandRatioSum = 0;
+ float rowSum = getExpandRatioSum(rowExpandRatio);
+ if (rowSum == 0) {
+ // no rows have been expanded
+ float equalSize = 1 / (float) getRows();
+ int myRatio = Math.round(equalSize * 1000);
+ for (int i = 0; i < getRows(); i++) {
+ rowExpandRatioArray[i] = myRatio;
+ }
+ realRowExpandRatioSum = myRatio * getRows();
+ } else {
+ for (int cury = 0; cury < getRows(); cury++) {
+ int myRatio = Math
+ .round((getRowExpandRatio(cury) / rowSum) * 1000);
+ rowExpandRatioArray[cury] = myRatio;
+ realRowExpandRatioSum += myRatio;
+ }
+ }
+
+ // correct possible rounding error
+ if (rowExpandRatioArray.length > 0) {
+ rowExpandRatioArray[0] -= realRowExpandRatioSum - 1000;
+ }
+ if (columnExpandRatioArray.length > 0) {
+ columnExpandRatioArray[0] -= realColExpandRatioSum - 1000;
+ }
+ target.addAttribute("colExpand", columnExpandRatioArray);
+ target.addAttribute("rowExpand", rowExpandRatioArray);
+
+ }
+
+ private float getExpandRatioSum(Map<Integer, Float> ratioMap) {
+ float sum = 0;
+ for (Iterator<Entry<Integer, Float>> iterator = ratioMap.entrySet()
+ .iterator(); iterator.hasNext();) {
+ sum += iterator.next().getValue();
+ }
+ return sum;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com
+ * .vaadin.ui.Component)
+ */
+ @Override
+ public Alignment getComponentAlignment(Component childComponent) {
+ ChildComponentData childComponentData = getState(false).childData
+ .get(childComponent);
+ if (childComponentData == null) {
+ throw new IllegalArgumentException(
+ "The given component is not a child of this layout");
+ } else {
+ return new Alignment(childComponentData.alignment);
+ }
+ }
+
+ /**
+ * Defines a rectangular area of cells in a GridLayout.
+ *
+ * <p>
+ * Also maintains a reference to the component contained in the area.
+ * </p>
+ *
+ * <p>
+ * The area is specified by the cell coordinates of its upper left corner
+ * (column1,row1) and lower right corner (column2,row2). As otherwise with
+ * GridLayout, the column and row coordinates start from zero.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+ public class Area implements Serializable {
+ private final ChildComponentData childData;
+ private final Component component;
+
+ /**
+ * <p>
+ * Construct a new area on a grid.
+ * </p>
+ *
+ * @param component
+ * the component connected to the area.
+ * @param column1
+ * The column of the upper left corner cell of the area. The
+ * leftmost column has index 0.
+ * @param row1
+ * The row of the upper left corner cell of the area. The
+ * topmost row has index 0.
+ * @param column2
+ * The column of the lower right corner cell of the area. The
+ * leftmost column has index 0.
+ * @param row2
+ * The row of the lower right corner cell of the area. The
+ * topmost row has index 0.
+ */
+ public Area(Component component, int column1, int row1, int column2,
+ int row2) {
+ this.component = component;
+ childData = new ChildComponentData();
+ childData.alignment = getDefaultComponentAlignment().getBitMask();
+ childData.column1 = column1;
+ childData.row1 = row1;
+ childData.column2 = column2;
+ childData.row2 = row2;
+ }
+
+ public Area(ChildComponentData childData, Component component) {
+ this.childData = childData;
+ this.component = component;
+ }
+
+ /**
+ * Tests if this Area overlaps with another Area.
+ *
+ * @param other
+ * the other Area that is to be tested for overlap with this
+ * area
+ * @return <code>true</code> if <code>other</code> area overlaps with
+ * this on, <code>false</code> if it does not.
+ */
+ public boolean overlaps(Area other) {
+ return componentsOverlap(childData, other.childData);
+ }
+
+ /**
+ * Gets the component connected to the area.
+ *
+ * @return the Component.
+ */
+ public Component getComponent() {
+ return component;
+ }
+
+ /**
+ * Gets the column of the top-left corner cell.
+ *
+ * @return the column of the top-left corner cell.
+ */
+ public int getColumn1() {
+ return childData.column1;
+ }
+
+ /**
+ * Gets the column of the bottom-right corner cell.
+ *
+ * @return the column of the bottom-right corner cell.
+ */
+ public int getColumn2() {
+ return childData.column2;
+ }
+
+ /**
+ * Gets the row of the top-left corner cell.
+ *
+ * @return the row of the top-left corner cell.
+ */
+ public int getRow1() {
+ return childData.row1;
+ }
+
+ /**
+ * Gets the row of the bottom-right corner cell.
+ *
+ * @return the row of the bottom-right corner cell.
+ */
+ public int getRow2() {
+ return childData.row2;
+ }
+
+ }
+
+ private static boolean componentsOverlap(ChildComponentData a,
+ ChildComponentData b) {
+ return a.column1 <= b.column2 && a.row1 <= b.row2
+ && a.column2 >= b.column1 && a.row2 >= b.row1;
+ }
+
+ /**
+ * Gridlayout does not support laying components on top of each other. An
+ * <code>OverlapsException</code> is thrown when a component already exists
+ * (even partly) at the same space on a grid with the new component.
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+ public class OverlapsException extends java.lang.RuntimeException {
+
+ private final Area existingArea;
+
+ /**
+ * Constructs an <code>OverlapsException</code>.
+ *
+ * @param existingArea
+ */
+ public OverlapsException(Area existingArea) {
+ this.existingArea = existingArea;
+ }
+
+ @Override
+ public String getMessage() {
+ StringBuilder sb = new StringBuilder();
+ Component component = existingArea.getComponent();
+ sb.append(component);
+ sb.append("( type = ");
+ sb.append(component.getClass().getName());
+ if (component.getCaption() != null) {
+ sb.append(", caption = \"");
+ sb.append(component.getCaption());
+ sb.append("\"");
+ }
+ sb.append(")");
+ sb.append(" is already added to ");
+ sb.append(existingArea.childData.column1);
+ sb.append(",");
+ sb.append(existingArea.childData.column1);
+ sb.append(",");
+ sb.append(existingArea.childData.row1);
+ sb.append(",");
+ sb.append(existingArea.childData.row2);
+ sb.append("(column1, column2, row1, row2).");
+
+ return sb.toString();
+ }
+
+ /**
+ * Gets the area .
+ *
+ * @return the existing area.
+ */
+ public Area getArea() {
+ return existingArea;
+ }
+ }
+
+ /**
+ * An <code>Exception</code> object which is thrown when an area exceeds the
+ * bounds of the grid.
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+ public class OutOfBoundsException extends java.lang.RuntimeException {
+
+ private final Area areaOutOfBounds;
+
+ /**
+ * Constructs an <code>OoutOfBoundsException</code> with the specified
+ * detail message.
+ *
+ * @param areaOutOfBounds
+ */
+ public OutOfBoundsException(Area areaOutOfBounds) {
+ this.areaOutOfBounds = areaOutOfBounds;
+ }
+
+ /**
+ * Gets the area that is out of bounds.
+ *
+ * @return the area out of Bound.
+ */
+ public Area getArea() {
+ return areaOutOfBounds;
+ }
+ }
+
+ /**
+ * Sets the number of columns in the grid. The column count can not be
+ * reduced if there are any areas that would be outside of the shrunk grid.
+ *
+ * @param columns
+ * the new number of columns in the grid.
+ */
+ public void setColumns(int columns) {
+
+ // The the param
+ if (columns < 1) {
+ throw new IllegalArgumentException(
+ "The number of columns and rows in the grid must be at least 1");
+ }
+
+ // In case of no change
+ if (getColumns() == columns) {
+ return;
+ }
+
+ // Checks for overlaps
+ if (getColumns() > columns) {
+ for (Entry<Connector, ChildComponentData> entry : getState().childData
+ .entrySet()) {
+ if (entry.getValue().column2 >= columns) {
+ throw new OutOfBoundsException(new Area(entry.getValue(),
+ (Component) entry.getKey()));
+ }
+ }
+ }
+
+ // Forget expands for removed columns
+ if (columns < getColumns()) {
+ for (int i = columns - 1; i < getColumns(); i++) {
+ columnExpandRatio.remove(i);
+ getState().explicitColRatios.remove(i);
+ }
+ }
+
+ getState().columns = columns;
+ }
+
+ /**
+ * Get the number of columns in the grid.
+ *
+ * @return the number of columns in the grid.
+ */
+ public int getColumns() {
+ return getState(false).columns;
+ }
+
+ /**
+ * Sets the number of rows in the grid. The number of rows can not be
+ * reduced if there are any areas that would be outside of the shrunk grid.
+ *
+ * @param rows
+ * the new number of rows in the grid.
+ */
+ public void setRows(int rows) {
+
+ // The the param
+ if (rows < 1) {
+ throw new IllegalArgumentException(
+ "The number of columns and rows in the grid must be at least 1");
+ }
+
+ // In case of no change
+ if (getRows() == rows) {
+ return;
+ }
+
+ // Checks for overlaps
+ if (getRows() > rows) {
+ for (Entry<Connector, ChildComponentData> entry : getState().childData
+ .entrySet()) {
+ if (entry.getValue().row2 >= rows) {
+ throw new OutOfBoundsException(new Area(entry.getValue(),
+ (Component) entry.getKey()));
+ }
+ }
+ }
+ // Forget expands for removed rows
+ if (rows < getRows()) {
+ for (int i = rows - 1; i < getRows(); i++) {
+ rowExpandRatio.remove(i);
+ getState().explicitRowRatios.remove(i);
+ }
+ }
+
+ getState().rows = rows;
+ }
+
+ /**
+ * Get the number of rows in the grid.
+ *
+ * @return the number of rows in the grid.
+ */
+ public int getRows() {
+ return getState(false).rows;
+ }
+
+ /**
+ * Gets the current x-position (column) of the cursor.
+ *
+ * <p>
+ * The cursor position points the position for the next component that is
+ * added without specifying its coordinates (grid cell). When the cursor
+ * position is occupied, the next component will be added to first free
+ * position after the cursor.
+ * </p>
+ *
+ * @return the grid column the cursor is on, starting from 0.
+ */
+ public int getCursorX() {
+ return cursorX;
+ }
+
+ /**
+ * Sets the current cursor x-position. This is usually handled automatically
+ * by GridLayout.
+ *
+ * @param cursorX
+ */
+ public void setCursorX(int cursorX) {
+ this.cursorX = cursorX;
+ }
+
+ /**
+ * Gets the current y-position (row) of the cursor.
+ *
+ * <p>
+ * The cursor position points the position for the next component that is
+ * added without specifying its coordinates (grid cell). When the cursor
+ * position is occupied, the next component will be added to the first free
+ * position after the cursor.
+ * </p>
+ *
+ * @return the grid row the Cursor is on.
+ */
+ public int getCursorY() {
+ return cursorY;
+ }
+
+ /**
+ * Sets the current y-coordinate (row) of the cursor. This is usually
+ * handled automatically by GridLayout.
+ *
+ * @param cursorY
+ * the row number, starting from 0 for the topmost row.
+ */
+ public void setCursorY(int cursorY) {
+ this.cursorY = cursorY;
+ }
+
+ /* Documented in superclass */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+
+ // Gets the locations
+ ChildComponentData oldLocation = getState().childData.get(oldComponent);
+ ChildComponentData newLocation = getState().childData.get(newComponent);
+
+ if (oldLocation == null) {
+ addComponent(newComponent);
+ } else if (newLocation == null) {
+ removeComponent(oldComponent);
+ addComponent(newComponent, oldLocation.column1, oldLocation.row1,
+ oldLocation.column2, oldLocation.row2);
+ } else {
+ int oldAlignment = oldLocation.alignment;
+ oldLocation.alignment = newLocation.alignment;
+ newLocation.alignment = oldAlignment;
+
+ getState().childData.put(newComponent, oldLocation);
+ getState().childData.put(oldComponent, newLocation);
+ }
+ }
+
+ /*
+ * Removes all components from this container.
+ *
+ * @see com.vaadin.ui.ComponentContainer#removeAllComponents()
+ */
+ @Override
+ public void removeAllComponents() {
+ super.removeAllComponents();
+ cursorX = 0;
+ cursorY = 0;
+ }
+
+ @Override
+ public void setComponentAlignment(Component childComponent,
+ Alignment alignment) {
+ ChildComponentData childComponentData = getState().childData
+ .get(childComponent);
+ if (childComponentData == null) {
+ throw new IllegalArgumentException(
+ "Component must be added to layout before using setComponentAlignment()");
+ } else {
+ if (alignment == null) {
+ childComponentData.alignment = GridLayoutState.ALIGNMENT_DEFAULT
+ .getBitMask();
+ } else {
+ childComponentData.alignment = alignment.getBitMask();
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean)
+ */
+ @Override
+ public void setSpacing(boolean spacing) {
+ getState().spacing = spacing;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing()
+ */
+ @Override
+ public boolean isSpacing() {
+ return getState(false).spacing;
+ }
+
+ /**
+ * Inserts an empty row at the specified position in the grid.
+ *
+ * @param row
+ * Index of the row before which the new row will be inserted.
+ * The leftmost row has index 0.
+ */
+ public void insertRow(int row) {
+ if (row > getRows()) {
+ throw new IllegalArgumentException("Cannot insert row at " + row
+ + " in a gridlayout with height " + getRows());
+ }
+
+ for (ChildComponentData existingArea : getState().childData.values()) {
+ // Areas ending below the row needs to be moved down or stretched
+ if (existingArea.row2 >= row) {
+ existingArea.row2++;
+
+ // Stretch areas that span over the selected row
+ if (existingArea.row1 >= row) {
+ existingArea.row1++;
+ }
+
+ }
+ }
+
+ if (cursorY >= row) {
+ cursorY++;
+ }
+
+ setRows(getRows() + 1);
+ markAsDirty();
+ }
+
+ /**
+ * Removes a row and all the components in the row.
+ *
+ * <p>
+ * Components which span over several rows are removed if the selected row
+ * is on the first row of such a component.
+ * </p>
+ *
+ * <p>
+ * If the last row is removed then all remaining components will be removed
+ * and the grid will be reduced to one row. The cursor will be moved to the
+ * upper left cell of the grid.
+ * </p>
+ *
+ * @param row
+ * Index of the row to remove. The leftmost row has index 0.
+ */
+ public void removeRow(int row) {
+ if (row >= getRows()) {
+ throw new IllegalArgumentException("Cannot delete row " + row
+ + " from a gridlayout with height " + getRows());
+ }
+
+ // Remove all components in row
+ for (int col = 0; col < getColumns(); col++) {
+ removeComponent(col, row);
+ }
+
+ // Shrink or remove areas in the selected row
+ for (ChildComponentData existingArea : getState().childData.values()) {
+ if (existingArea.row2 >= row) {
+ existingArea.row2--;
+
+ if (existingArea.row1 > row) {
+ existingArea.row1--;
+ }
+ }
+ }
+
+ if (getRows() == 1) {
+ /*
+ * Removing the last row means that the dimensions of the Grid
+ * layout will be truncated to 1 empty row and the cursor is moved
+ * to the first cell
+ */
+ cursorX = 0;
+ cursorY = 0;
+ } else {
+ setRows(getRows() - 1);
+ if (cursorY > row) {
+ cursorY--;
+ }
+ }
+
+ markAsDirty();
+
+ }
+
+ /**
+ * Sets the expand ratio of given column.
+ *
+ * <p>
+ * The expand ratio defines how excess space is distributed among columns.
+ * Excess space means space that is left over from components that are not
+ * sized relatively. By default, the excess space is distributed evenly.
+ * </p>
+ *
+ * <p>
+ * Note that the component width of the GridLayout must be defined (fixed or
+ * relative, as opposed to undefined) for this method to have any effect.
+ * </p>
+ *
+ * @see #setWidth(float, int)
+ *
+ * @param columnIndex
+ * @param ratio
+ */
+ public void setColumnExpandRatio(int columnIndex, float ratio) {
+ columnExpandRatio.put(columnIndex, ratio);
+ getState().explicitColRatios.add(columnIndex);
+ markAsDirty();
+ }
+
+ /**
+ * Returns the expand ratio of given column
+ *
+ * @see #setColumnExpandRatio(int, float)
+ *
+ * @param columnIndex
+ * @return the expand ratio, 0.0f by default
+ */
+ public float getColumnExpandRatio(int columnIndex) {
+ Float r = columnExpandRatio.get(columnIndex);
+ return r == null ? 0 : r.floatValue();
+ }
+
+ /**
+ * Sets the expand ratio of given row.
+ *
+ * <p>
+ * Expand ratio defines how excess space is distributed among rows. Excess
+ * space means the space left over from components that are not sized
+ * relatively. By default, the excess space is distributed evenly.
+ * </p>
+ *
+ * <p>
+ * Note, that height needs to be defined (fixed or relative, as opposed to
+ * undefined height) for this method to have any effect.
+ * </p>
+ *
+ * @see #setHeight(float, int)
+ *
+ * @param rowIndex
+ * The row index, starting from 0 for the topmost row.
+ * @param ratio
+ */
+ public void setRowExpandRatio(int rowIndex, float ratio) {
+ rowExpandRatio.put(rowIndex, ratio);
+ getState().explicitRowRatios.add(rowIndex);
+ markAsDirty();
+ }
+
+ /**
+ * Returns the expand ratio of given row.
+ *
+ * @see #setRowExpandRatio(int, float)
+ *
+ * @param rowIndex
+ * The row index, starting from 0 for the topmost row.
+ * @return the expand ratio, 0.0f by default
+ */
+ public float getRowExpandRatio(int rowIndex) {
+ Float r = rowExpandRatio.get(rowIndex);
+ return r == null ? 0 : r.floatValue();
+ }
+
+ /**
+ * Gets the Component at given index.
+ *
+ * @param x
+ * The column index, starting from 0 for the leftmost column.
+ * @param y
+ * The row index, starting from 0 for the topmost row.
+ * @return Component in given cell or null if empty
+ */
+ public Component getComponent(int x, int y) {
+ for (Entry<Connector, ChildComponentData> entry : getState(false).childData
+ .entrySet()) {
+ ChildComponentData childData = entry.getValue();
+ if (childData.column1 <= x && x <= childData.column2
+ && childData.row1 <= y && y <= childData.row2) {
+ return (Component) entry.getKey();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns information about the area where given component is laid in the
+ * GridLayout.
+ *
+ * @param component
+ * the component whose area information is requested.
+ * @return an Area object that contains information how component is laid in
+ * the grid
+ */
+ public Area getComponentArea(Component component) {
+ ChildComponentData childComponentData = getState(false).childData
+ .get(component);
+ if (childComponentData == null) {
+ return null;
+ } else {
+ return new Area(childComponentData, component);
+ }
+ }
+
+ @Override
+ public void addLayoutClickListener(LayoutClickListener listener) {
+ addListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener,
+ LayoutClickListener.clickMethod);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addLayoutClickListener(LayoutClickListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(LayoutClickListener listener) {
+ addLayoutClickListener(listener);
+ }
+
+ @Override
+ public void removeLayoutClickListener(LayoutClickListener listener) {
+ removeListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeLayoutClickListener(LayoutClickListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(LayoutClickListener listener) {
+ removeLayoutClickListener(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.MarginHandler#setMargin(boolean)
+ */
+ @Override
+ public void setMargin(boolean enabled) {
+ setMargin(new MarginInfo(enabled));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.ui.Layout.MarginHandler#setMargin(com.vaadin.shared.ui.MarginInfo
+ * )
+ */
+ @Override
+ public void setMargin(MarginInfo marginInfo) {
+ getState().marginsBitmask = marginInfo.getBitMask();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.MarginHandler#getMargin()
+ */
+ @Override
+ public MarginInfo getMargin() {
+ return new MarginInfo(getState(false).marginsBitmask);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.AlignmentHandler#getDefaultComponentAlignment()
+ */
+ @Override
+ public Alignment getDefaultComponentAlignment() {
+ return defaultComponentAlignment;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.ui.Layout.AlignmentHandler#setDefaultComponentAlignment(com
+ * .vaadin.ui.Alignment)
+ */
+ @Override
+ public void setDefaultComponentAlignment(Alignment defaultAlignment) {
+ defaultComponentAlignment = defaultAlignment;
+ }
+
+ /**
+ * Sets whether empty rows and columns should be considered as non-existent
+ * when rendering or not. If this is set to true then the spacing between
+ * multiple empty columns (or rows) will be collapsed.
+ *
+ * The default behavior is to consider all rows and columns as visible
+ *
+ * NOTE that this must be set before the initial rendering takes place.
+ * Updating this on the fly is not supported.
+ *
+ * @since 7.3
+ * @param hideEmptyRowsAndColumns
+ * true to hide empty rows and columns, false to leave them as-is
+ */
+ public void setHideEmptyRowsAndColumns(boolean hideEmptyRowsAndColumns) {
+ getState().hideEmptyRowsAndColumns = hideEmptyRowsAndColumns;
+ }
+
+ /**
+ * Checks whether whether empty rows and columns should be considered as
+ * non-existent when rendering or not.
+ *
+ * @see #setHideEmptyRowsAndColumns(boolean)
+ * @since 7.3
+ * @return true if empty rows and columns are hidden, false otherwise
+ */
+ public boolean isHideEmptyRowsAndColumns() {
+ return getState(false).hideEmptyRowsAndColumns;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * After reading the design, cursorY is set to point to a row outside of the
+ * GridLayout area. CursorX is reset to 0.
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+
+ setMargin(readMargin(design, getMargin(), designContext));
+
+ List<Element> rowElements = new ArrayList<Element>();
+ List<Map<Integer, Component>> rows = new ArrayList<Map<Integer, Component>>();
+ // Prepare a 2D map for reading column contents
+ for (Element e : design.children()) {
+ if (e.tagName().equalsIgnoreCase("row")) {
+ rowElements.add(e);
+ rows.add(new HashMap<Integer, Component>());
+
+ }
+ }
+ setRows(Math.max(rows.size(), 1));
- child = designContext.readDesign(col.child(0));
++ Map<Component, Alignment> alignments = new HashMap<Component, Alignment>();
+ List<Integer> columnExpandRatios = new ArrayList<Integer>();
+ for (int row = 0; row < rowElements.size(); ++row) {
+ Element rowElement = rowElements.get(row);
+
+ // Row Expand
+ if (rowElement.hasAttr("expand")) {
+ int expand = DesignAttributeHandler.readAttribute("expand",
+ rowElement.attributes(), int.class);
+ setRowExpandRatio(row, expand);
+ }
+
+ Elements cols = rowElement.children();
+
+ // Amount of skipped columns due to spanned components
+ int skippedColumns = 0;
+
+ for (int column = 0; column < cols.size(); ++column) {
+ while (rows.get(row).containsKey(column + skippedColumns)) {
+ // Skip any spanned components
+ skippedColumns++;
+ }
+
+ Element col = cols.get(column);
+ Component child = null;
+
+ if (col.children().size() > 0) {
- if (alignment.isMiddle()) {
- childElement.attr(":middle", true);
- } else if (alignment.isBottom()) {
- childElement.attr(":bottom", true);
- }
- if (alignment.isCenter()) {
- childElement.attr(":center", true);
- } else if (alignment.isRight()) {
- childElement.attr(":right", true);
- }
++ Element childElement = col.child(0);
++ child = designContext.readDesign(childElement);
++ alignments.put(child, DesignAttributeHandler
++ .readAlignment(childElement.attributes()));
+ // TODO: Currently ignoring any extra children.
+ // Needs Error handling?
+ } // Else: Empty placeholder. No child component.
+
+ // Handle rowspan and colspan for this child component
+ Attributes attr = col.attributes();
+ int colspan = DesignAttributeHandler.readAttribute("colspan",
+ attr, 1, int.class);
+ int rowspan = DesignAttributeHandler.readAttribute("rowspan",
+ attr, 1, int.class);
+
+ for (int rowIndex = row; rowIndex < row + rowspan; ++rowIndex) {
+ for (int colIndex = column; colIndex < column + colspan; ++colIndex) {
+ if (rowIndex == rows.size()) {
+ // Rowspan with not enough rows. Fix by adding rows.
+ rows.add(new HashMap<Integer, Component>());
+ }
+ rows.get(rowIndex)
+ .put(colIndex + skippedColumns, child);
+ }
+ }
+
+ // Read column expand ratios if handling the first row.
+ if (row == 0) {
+ if (col.hasAttr("expand")) {
+ for (String expand : col.attr("expand").split(",")) {
+ columnExpandRatios.add(Integer.parseInt(expand));
+ }
+ } else {
+ for (int c = 0; c < colspan; ++c) {
+ columnExpandRatios.add(0);
+ }
+ }
+ }
+
+ skippedColumns += (colspan - 1);
+ }
+ }
+
+ // Calculate highest column count and set columns
+ int colMax = 0;
+ for (Map<Integer, Component> cols : rows) {
+ if (colMax < cols.size()) {
+ colMax = cols.size();
+ }
+ }
+ setColumns(Math.max(colMax, 1));
+
+ for (int i = 0; i < columnExpandRatios.size(); ++i) {
+ setColumnExpandRatio(i, columnExpandRatios.get(i));
+ }
+
+ // Reiterate through the 2D map and add components to GridLayout
+ Set<Component> visited = new HashSet<Component>();
+
+ // Ignore any missing components
+ visited.add(null);
+
+ for (int i = 0; i < rows.size(); ++i) {
+ Map<Integer, Component> row = rows.get(i);
+ for (int j = 0; j < colMax; ++j) {
+ Component child = row.get(j);
+ if (visited.contains(child)) {
+ // Empty location or already handled child
+ continue;
+ }
+ visited.add(child);
+
+ // Figure out col and rowspan from 2D map
+ int colspan = 0;
+ while (j + colspan + 1 < row.size()
+ && row.get(j + colspan + 1) == child) {
+ ++colspan;
+ }
+
+ int rowspan = 0;
+ while (i + rowspan + 1 < rows.size()
+ && rows.get(i + rowspan + 1).get(j) == child) {
+ ++rowspan;
+ }
+
+ // Add component with area
+ addComponent(child, j, i, j + colspan, i + rowspan);
++ setComponentAlignment(child, alignments.get(child));
+ }
+ }
+ // Set cursor position explicitly
+ setCursorY(getRows());
+ setCursorX(0);
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+
+ GridLayout def = designContext.getDefaultInstance(this);
+
+ writeMargin(design, getMargin(), def.getMargin(), designContext);
+
+ if (!designContext.shouldWriteChildren(this, def)) {
+ return;
+ }
+
+ if (components.isEmpty()) {
+ writeEmptyColsAndRows(design, designContext);
+ return;
+ }
+
+ final Map<Connector, ChildComponentData> childData = getState().childData;
+
+ // Make a 2D map of component areas.
+ Component[][] componentMap = new Component[getState().rows][getState().columns];
+ final Component dummyComponent = new Label("");
+
+ for (Component component : components) {
+ ChildComponentData coords = childData.get(component);
+ for (int row = coords.row1; row <= coords.row2; ++row) {
+ for (int col = coords.column1; col <= coords.column2; ++col) {
+ componentMap[row][col] = component;
+ }
+ }
+ }
+
+ // Go through the map and write only needed column tags
+ Set<Connector> visited = new HashSet<Connector>();
+
+ // Skip the dummy placeholder
+ visited.add(dummyComponent);
+
+ for (int i = 0; i < componentMap.length; ++i) {
+ Element row = design.appendElement("row");
+
+ // Row Expand
+ DesignAttributeHandler.writeAttribute("expand", row.attributes(),
+ (int) getRowExpandRatio(i), 0, int.class);
+
+ int colspan = 1;
+ Element col;
+ for (int j = 0; j < componentMap[i].length; ++j) {
+ Component child = componentMap[i][j];
+ if (child != null) {
+ if (visited.contains(child)) {
+ // Child has already been written in the design
+ continue;
+ }
+ visited.add(child);
+
+ Element childElement = designContext.createElement(child);
+ col = row.appendElement("column");
+
+ // Write child data into design
+ ChildComponentData coords = childData.get(child);
+
+ Alignment alignment = getComponentAlignment(child);
++ DesignAttributeHandler.writeAlignment(childElement,
++ alignment);
+
+ col.appendChild(childElement);
+ if (coords.row1 != coords.row2) {
+ col.attr("rowspan", ""
+ + (1 + coords.row2 - coords.row1));
+ }
+
+ colspan = 1 + coords.column2 - coords.column1;
+ if (colspan > 1) {
+ col.attr("colspan", "" + colspan);
+ }
+
+ } else {
+ boolean hasExpands = false;
+ if (i == 0
+ && lastComponentOnRow(componentMap[i], j, visited)) {
+ // A column with expand and no content in the end of
+ // first row needs to be present.
+ for (int c = j; c < componentMap[i].length; ++c) {
+ if ((int) getColumnExpandRatio(c) > 0) {
+ hasExpands = true;
+ }
+ }
+ }
+
+ if (lastComponentOnRow(componentMap[i], j, visited)
+ && !hasExpands) {
+ continue;
+ }
+
+ // Empty placeholder tag.
+ col = row.appendElement("column");
+
+ // Use colspan to make placeholders more pleasant
+ while (j + colspan < componentMap[i].length
+ && componentMap[i][j + colspan] == child) {
+ ++colspan;
+ }
+
+ int rowspan = getRowSpan(componentMap, i, j, colspan, child);
+ if (colspan > 1) {
+ col.attr("colspan", "" + colspan);
+ }
+ if (rowspan > 1) {
+ col.attr("rowspan", "" + rowspan);
+ }
+ for (int x = 0; x < rowspan; ++x) {
+ for (int y = 0; y < colspan; ++y) {
+ // Mark handled columns
+ componentMap[i + x][j + y] = dummyComponent;
+ }
+ }
+ }
+
+ // Column expands
+ if (i == 0) {
+ // Only do expands on first row
+ String expands = "";
+ boolean expandRatios = false;
+ for (int c = 0; c < colspan; ++c) {
+ int colExpand = (int) getColumnExpandRatio(j + c);
+ if (colExpand > 0) {
+ expandRatios = true;
+ }
+ expands += (c > 0 ? "," : "") + colExpand;
+ }
+ if (expandRatios) {
+ col.attr("expand", expands);
+ }
+ }
+
+ j += colspan - 1;
+ }
+ }
+ }
+
+ /**
+ * Fills in the design with rows and empty columns. This needs to be done
+ * for empty {@link GridLayout}, because there's no other way to serialize
+ * info about number of columns and rows if there are absolutely no
+ * components in the {@link GridLayout}
+ *
+ * @param design
+ * @param designContext
+ */
+ private void writeEmptyColsAndRows(Element design,
+ DesignContext designContext) {
+ int rowCount = getState(false).rows;
+ int colCount = getState(false).columns;
+
+ // only write cols and rows tags if size is not 1x1
+ if (rowCount == 1 && colCount == 1) {
+ return;
+ }
+
+ for (int i = 0; i < rowCount; i++) {
+ Element row = design.appendElement("row");
+ for (int j = 0; j < colCount; j++) {
+ row.appendElement("column");
+ }
+ }
+
+ }
+
+ private int getRowSpan(Component[][] compMap, int i, int j, int colspan,
+ Component child) {
+ int rowspan = 1;
+ while (i + rowspan < compMap.length && compMap[i + rowspan][j] == child) {
+ for (int k = 0; k < colspan; ++k) {
+ if (compMap[i + rowspan][j + k] != child) {
+ return rowspan;
+ }
+ }
+ rowspan++;
+ }
+ return rowspan;
+ }
+
+ private boolean lastComponentOnRow(Component[] componentArray, int j,
+ Set<Connector> visited) {
+ while ((++j) < componentArray.length) {
+ Component child = componentArray[j];
+ if (child != null && !visited.contains(child)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add("cursor-x");
+ result.add("cursor-y");
+ result.add("rows");
+ result.add("columns");
+ return result;
+ }
+}
--- /dev/null
- window.close();
+/*
+ * Copyright 2000-2014 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.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.BlurNotifier;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.event.FieldEvents.FocusNotifier;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.event.ShortcutAction;
+import com.vaadin.event.ShortcutAction.KeyCode;
+import com.vaadin.event.ShortcutAction.ModifierKey;
+import com.vaadin.event.ShortcutListener;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.window.WindowMode;
+import com.vaadin.shared.ui.window.WindowRole;
+import com.vaadin.shared.ui.window.WindowServerRpc;
+import com.vaadin.shared.ui.window.WindowState;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * A component that represents a floating popup window that can be added to a
+ * {@link UI}. A window is added to a {@code UI} using
+ * {@link UI#addWindow(Window)}. </p>
+ * <p>
+ * The contents of a window is set using {@link #setContent(Component)} or by
+ * using the {@link #Window(String, Component)} constructor.
+ * </p>
+ * <p>
+ * A window can be positioned on the screen using absolute coordinates (pixels)
+ * or set to be centered using {@link #center()}
+ * </p>
+ * <p>
+ * The caption is displayed in the window header.
+ * </p>
+ * <p>
+ * In Vaadin versions prior to 7.0.0, Window was also used as application level
+ * windows. This function is now covered by the {@link UI} class.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+@SuppressWarnings({ "serial", "deprecation" })
+public class Window extends Panel implements FocusNotifier, BlurNotifier,
+ LegacyComponent {
+
+ private WindowServerRpc rpc = new WindowServerRpc() {
+
+ @Override
+ public void click(MouseEventDetails mouseDetails) {
+ fireEvent(new ClickEvent(Window.this, mouseDetails));
+ }
+
+ @Override
+ public void windowModeChanged(WindowMode newState) {
+ setWindowMode(newState);
+ }
+
+ @Override
+ public void windowMoved(int x, int y) {
+ if (x != getState(false).positionX) {
+ setPositionX(x);
+ }
+ if (y != getState(false).positionY) {
+ setPositionY(y);
+ }
+ }
+ };
+
+ /**
+ * Holds registered CloseShortcut instances for query and later removal
+ */
+ private List<CloseShortcut> closeShortcuts = new ArrayList<CloseShortcut>(4);
+
+ /**
+ * Creates a new, empty window
+ */
+ public Window() {
+ this("", null);
+ }
+
+ /**
+ * Creates a new, empty window with a given title.
+ *
+ * @param caption
+ * the title of the window.
+ */
+ public Window(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * Creates a new, empty window with the given content and title.
+ *
+ * @param caption
+ * the title of the window.
+ * @param content
+ * the contents of the window
+ */
+ public Window(String caption, Component content) {
+ super(caption, content);
+ registerRpc(rpc);
+ setSizeUndefined();
+ setCloseShortcut(KeyCode.ESCAPE);
+ }
+
+ /* ********************************************************************* */
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Panel#paintContent(com.vaadin.server.PaintTarget)
+ */
+
+ @Override
+ public synchronized void paintContent(PaintTarget target)
+ throws PaintException {
+ if (bringToFront != null) {
+ target.addAttribute("bringToFront", bringToFront.intValue());
+ bringToFront = null;
+ }
+
+ // Contents of the window panel is painted
+ super.paintContent(target);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.ui.AbstractComponent#setParent(com.vaadin.server.ClientConnector
+ * )
+ */
+ @Override
+ public void setParent(HasComponents parent) {
+ if (parent == null || parent instanceof UI) {
+ super.setParent(parent);
+ } else {
+ throw new IllegalArgumentException(
+ "A Window can only be added to a UI using UI.addWindow(Window window)");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Panel#changeVariables(java.lang.Object, java.util.Map)
+ */
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ // TODO Are these for top level windows or sub windows?
+ boolean sizeHasChanged = false;
+ // size is handled in super class, but resize events only in windows ->
+ // so detect if size change occurs before super.changeVariables()
+ if (variables.containsKey("height")
+ && (getHeightUnits() != Unit.PIXELS || (Integer) variables
+ .get("height") != getHeight())) {
+ sizeHasChanged = true;
+ }
+ if (variables.containsKey("width")
+ && (getWidthUnits() != Unit.PIXELS || (Integer) variables
+ .get("width") != getWidth())) {
+ sizeHasChanged = true;
+ }
+
+ super.changeVariables(source, variables);
+
+ // Positioning
+ final Integer positionx = (Integer) variables.get("positionx");
+ if (positionx != null) {
+ final int x = positionx.intValue();
+ // This is information from the client so it is already using the
+ // position. No need to repaint.
+ setPositionX(x < 0 ? -1 : x);
+ }
+ final Integer positiony = (Integer) variables.get("positiony");
+ if (positiony != null) {
+ final int y = positiony.intValue();
+ // This is information from the client so it is already using the
+ // position. No need to repaint.
+ setPositionY(y < 0 ? -1 : y);
+ }
+
+ if (isClosable()) {
+ // Closing
+ final Boolean close = (Boolean) variables.get("close");
+ if (close != null && close.booleanValue()) {
+ close();
+ }
+ }
+
+ // fire event if size has really changed
+ if (sizeHasChanged) {
+ fireResize();
+ }
+
+ if (variables.containsKey(FocusEvent.EVENT_ID)) {
+ fireEvent(new FocusEvent(this));
+ } else if (variables.containsKey(BlurEvent.EVENT_ID)) {
+ fireEvent(new BlurEvent(this));
+ }
+
+ }
+
+ /**
+ * Method that handles window closing (from UI).
+ *
+ * <p>
+ * By default, windows are removed from their respective UIs and thus
+ * visually closed on browser-side.
+ * </p>
+ *
+ * <p>
+ * To react to a window being closed (after it is closed), register a
+ * {@link CloseListener}.
+ * </p>
+ */
+ public void close() {
+ UI uI = getUI();
+
+ // Don't do anything if not attached to a UI
+ if (uI != null) {
+ // window is removed from the UI
+ uI.removeWindow(this);
+ }
+ }
+
+ /**
+ * Gets the distance of Window left border in pixels from left border of the
+ * containing (main window) when the window is in {@link WindowMode#NORMAL}.
+ *
+ * @return the Distance of Window left border in pixels from left border of
+ * the containing (main window).or -1 if unspecified
+ * @since 4.0.0
+ */
+ public int getPositionX() {
+ return getState(false).positionX;
+ }
+
+ /**
+ * Sets the position of the window on the screen using
+ * {@link #setPositionX(int)} and {@link #setPositionY(int)}
+ *
+ * @since 7.5
+ * @param x
+ * The new x coordinate for the window
+ * @param y
+ * The new y coordinate for the window
+ */
+ public void setPosition(int x, int y) {
+ setPositionX(x);
+ setPositionY(y);
+ }
+
+ /**
+ * Sets the distance of Window left border in pixels from left border of the
+ * containing (main window). Has effect only if in {@link WindowMode#NORMAL}
+ * mode.
+ *
+ * @param positionX
+ * the Distance of Window left border in pixels from left border
+ * of the containing (main window). or -1 if unspecified.
+ * @since 4.0.0
+ */
+ public void setPositionX(int positionX) {
+ getState().positionX = positionX;
+ getState().centered = false;
+ }
+
+ /**
+ * Gets the distance of Window top border in pixels from top border of the
+ * containing (main window) when the window is in {@link WindowMode#NORMAL}
+ * state, or when next set to that state.
+ *
+ * @return Distance of Window top border in pixels from top border of the
+ * containing (main window). or -1 if unspecified
+ *
+ * @since 4.0.0
+ */
+ public int getPositionY() {
+ return getState(false).positionY;
+ }
+
+ /**
+ * Sets the distance of Window top border in pixels from top border of the
+ * containing (main window). Has effect only if in {@link WindowMode#NORMAL}
+ * mode.
+ *
+ * @param positionY
+ * the Distance of Window top border in pixels from top border of
+ * the containing (main window). or -1 if unspecified
+ *
+ * @since 4.0.0
+ */
+ public void setPositionY(int positionY) {
+ getState().positionY = positionY;
+ getState().centered = false;
+ }
+
+ private static final Method WINDOW_CLOSE_METHOD;
+ static {
+ try {
+ WINDOW_CLOSE_METHOD = CloseListener.class.getDeclaredMethod(
+ "windowClose", new Class[] { CloseEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error, window close method not found");
+ }
+ }
+
+ public static class CloseEvent extends Component.Event {
+
+ /**
+ *
+ * @param source
+ */
+ public CloseEvent(Component source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Window.
+ *
+ * @return the window.
+ */
+ public Window getWindow() {
+ return (Window) getSource();
+ }
+ }
+
+ /**
+ * An interface used for listening to Window close events. Add the
+ * CloseListener to a window and
+ * {@link CloseListener#windowClose(CloseEvent)} will be called whenever the
+ * user closes the window.
+ *
+ * <p>
+ * Since Vaadin 6.5, removing a window using {@link #removeWindow(Window)}
+ * fires the CloseListener.
+ * </p>
+ */
+ public interface CloseListener extends Serializable {
+ /**
+ * Called when the user closes a window. Use
+ * {@link CloseEvent#getWindow()} to get a reference to the
+ * {@link Window} that was closed.
+ *
+ * @param e
+ * Event containing
+ */
+ public void windowClose(CloseEvent e);
+ }
+
+ /**
+ * Adds a CloseListener to the window.
+ *
+ * For a window the CloseListener is fired when the user closes it (clicks
+ * on the close button).
+ *
+ * For a browser level window the CloseListener is fired when the browser
+ * level window is closed. Note that closing a browser level window does not
+ * mean it will be destroyed. Also note that Opera does not send events like
+ * all other browsers and therefore the close listener might not be called
+ * if Opera is used.
+ *
+ * <p>
+ * Since Vaadin 6.5, removing windows using {@link #removeWindow(Window)}
+ * does fire the CloseListener.
+ * </p>
+ *
+ * @param listener
+ * the CloseListener to add.
+ */
+ public void addCloseListener(CloseListener listener) {
+ addListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addCloseListener(CloseListener)}
+ **/
+ @Deprecated
+ public void addListener(CloseListener listener) {
+ addCloseListener(listener);
+ }
+
+ /**
+ * Removes the CloseListener from the window.
+ *
+ * <p>
+ * For more information on CloseListeners see {@link CloseListener}.
+ * </p>
+ *
+ * @param listener
+ * the CloseListener to remove.
+ */
+ public void removeCloseListener(CloseListener listener) {
+ removeListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeCloseListener(CloseListener)}
+ **/
+ @Deprecated
+ public void removeListener(CloseListener listener) {
+ removeCloseListener(listener);
+ }
+
+ protected void fireClose() {
+ fireEvent(new Window.CloseEvent(this));
+ }
+
+ /**
+ * Event which is fired when the mode of the Window changes.
+ *
+ * @author Vaadin Ltd
+ * @since 7.1
+ *
+ */
+ public static class WindowModeChangeEvent extends Component.Event {
+
+ private final WindowMode windowMode;
+
+ /**
+ *
+ * @param source
+ */
+ public WindowModeChangeEvent(Component source, WindowMode windowMode) {
+ super(source);
+ this.windowMode = windowMode;
+ }
+
+ /**
+ * Gets the Window.
+ *
+ * @return the window
+ */
+ public Window getWindow() {
+ return (Window) getSource();
+ }
+
+ /**
+ * Gets the new window mode.
+ *
+ * @return the new mode
+ */
+ public WindowMode getWindowMode() {
+ return windowMode;
+ }
+ }
+
+ /**
+ * An interface used for listening to Window maximize / restore events. Add
+ * the WindowModeChangeListener to a window and
+ * {@link WindowModeChangeListener#windowModeChanged(WindowModeChangeEvent)}
+ * will be called whenever the window is maximized (
+ * {@link WindowMode#MAXIMIZED}) or restored ({@link WindowMode#NORMAL} ).
+ */
+ public interface WindowModeChangeListener extends Serializable {
+
+ public static final Method windowModeChangeMethod = ReflectTools
+ .findMethod(WindowModeChangeListener.class,
+ "windowModeChanged", WindowModeChangeEvent.class);
+
+ /**
+ * Called when the user maximizes / restores a window. Use
+ * {@link WindowModeChangeEvent#getWindow()} to get a reference to the
+ * {@link Window} that was maximized / restored. Use
+ * {@link WindowModeChangeEvent#getWindowMode()} to get a reference to
+ * the new state.
+ *
+ * @param event
+ */
+ public void windowModeChanged(WindowModeChangeEvent event);
+ }
+
+ /**
+ * Adds a WindowModeChangeListener to the window.
+ *
+ * The WindowModeChangeEvent is fired when the user changed the display
+ * state by clicking the maximize/restore button or by double clicking on
+ * the window header. The event is also fired if the state is changed using
+ * {@link #setWindowMode(WindowMode)}.
+ *
+ * @param listener
+ * the WindowModeChangeListener to add.
+ */
+ public void addWindowModeChangeListener(WindowModeChangeListener listener) {
+ addListener(WindowModeChangeEvent.class, listener,
+ WindowModeChangeListener.windowModeChangeMethod);
+ }
+
+ /**
+ * Removes the WindowModeChangeListener from the window.
+ *
+ * @param listener
+ * the WindowModeChangeListener to remove.
+ */
+ public void removeWindowModeChangeListener(WindowModeChangeListener listener) {
+ removeListener(WindowModeChangeEvent.class, listener,
+ WindowModeChangeListener.windowModeChangeMethod);
+ }
+
+ protected void fireWindowWindowModeChange() {
+ fireEvent(new Window.WindowModeChangeEvent(this, getState().windowMode));
+ }
+
+ /**
+ * Method for the resize event.
+ */
+ private static final Method WINDOW_RESIZE_METHOD;
+ static {
+ try {
+ WINDOW_RESIZE_METHOD = ResizeListener.class.getDeclaredMethod(
+ "windowResized", new Class[] { ResizeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error, window resized method not found");
+ }
+ }
+
+ /**
+ * Resize events are fired whenever the client-side fires a resize-event
+ * (e.g. the browser window is resized). The frequency may vary across
+ * browsers.
+ */
+ public static class ResizeEvent extends Component.Event {
+
+ /**
+ *
+ * @param source
+ */
+ public ResizeEvent(Component source) {
+ super(source);
+ }
+
+ /**
+ * Get the window form which this event originated
+ *
+ * @return the window
+ */
+ public Window getWindow() {
+ return (Window) getSource();
+ }
+ }
+
+ /**
+ * Listener for window resize events.
+ *
+ * @see com.vaadin.ui.Window.ResizeEvent
+ */
+ public interface ResizeListener extends Serializable {
+ public void windowResized(ResizeEvent e);
+ }
+
+ /**
+ * Add a resize listener.
+ *
+ * @param listener
+ */
+ public void addResizeListener(ResizeListener listener) {
+ addListener(ResizeEvent.class, listener, WINDOW_RESIZE_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addResizeListener(ResizeListener)}
+ **/
+ @Deprecated
+ public void addListener(ResizeListener listener) {
+ addResizeListener(listener);
+ }
+
+ /**
+ * Remove a resize listener.
+ *
+ * @param listener
+ */
+ public void removeResizeListener(ResizeListener listener) {
+ removeListener(ResizeEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeResizeListener(ResizeListener)}
+ **/
+ @Deprecated
+ public void removeListener(ResizeListener listener) {
+ removeResizeListener(listener);
+ }
+
+ /**
+ * Fire the resize event.
+ */
+ protected void fireResize() {
+ fireEvent(new ResizeEvent(this));
+ }
+
+ /**
+ * Used to keep the right order of windows if multiple windows are brought
+ * to front in a single changeset. If this is not used, the order is quite
+ * random (depends on the order getting to dirty list. e.g. which window got
+ * variable changes).
+ */
+ private Integer bringToFront = null;
+
+ /**
+ * If there are currently several windows visible, calling this method makes
+ * this window topmost.
+ * <p>
+ * This method can only be called if this window connected a UI. Else an
+ * illegal state exception is thrown. Also if there are modal windows and
+ * this window is not modal, and illegal state exception is thrown.
+ * <p>
+ */
+ public void bringToFront() {
+ UI uI = getUI();
+ if (uI == null) {
+ throw new IllegalStateException(
+ "Window must be attached to parent before calling bringToFront method.");
+ }
+ int maxBringToFront = -1;
+ for (Window w : uI.getWindows()) {
+ if (!isModal() && w.isModal()) {
+ throw new IllegalStateException(
+ "The UI contains modal windows, non-modal window cannot be brought to front.");
+ }
+ if (w.bringToFront != null) {
+ maxBringToFront = Math.max(maxBringToFront,
+ w.bringToFront.intValue());
+ }
+ }
+ bringToFront = Integer.valueOf(maxBringToFront + 1);
+ markAsDirty();
+ }
+
+ /**
+ * Sets window modality. When a modal window is open, components outside
+ * that window cannot be accessed.
+ * <p>
+ * Keyboard navigation is restricted by blocking the tab key at the top and
+ * bottom of the window by activating the tab stop function internally.
+ *
+ * @param modal
+ * true if modality is to be turned on
+ */
+ public void setModal(boolean modal) {
+ getState().modal = modal;
+ center();
+ }
+
+ /**
+ * @return true if this window is modal.
+ */
+ public boolean isModal() {
+ return getState(false).modal;
+ }
+
+ /**
+ * Sets window resizable.
+ *
+ * @param resizable
+ * true if resizability is to be turned on
+ */
+ public void setResizable(boolean resizable) {
+ getState().resizable = resizable;
+ }
+
+ /**
+ *
+ * @return true if window is resizable by the end-user, otherwise false.
+ */
+ public boolean isResizable() {
+ return getState(false).resizable;
+ }
+
+ /**
+ *
+ * @return true if a delay is used before recalculating sizes, false if
+ * sizes are recalculated immediately.
+ */
+ public boolean isResizeLazy() {
+ return getState(false).resizeLazy;
+ }
+
+ /**
+ * Should resize operations be lazy, i.e. should there be a delay before
+ * layout sizes are recalculated. Speeds up resize operations in slow UIs
+ * with the penalty of slightly decreased usability.
+ *
+ * Note, some browser send false resize events for the browser window and
+ * are therefore always lazy.
+ *
+ * @param resizeLazy
+ * true to use a delay before recalculating sizes, false to
+ * calculate immediately.
+ */
+ public void setResizeLazy(boolean resizeLazy) {
+ getState().resizeLazy = resizeLazy;
+ }
+
+ /**
+ * Sets this window to be centered relative to its parent window. Affects
+ * windows only. If the window is resized as a result of the size of its
+ * content changing, it will keep itself centered as long as its position is
+ * not explicitly changed programmatically or by the user.
+ * <p>
+ * <b>NOTE:</b> This method has several issues as currently implemented.
+ * Please refer to http://dev.vaadin.com/ticket/8971 for details.
+ */
+ public void center() {
+ getState().centered = true;
+ }
+
+ /**
+ * Returns the closable status of the window. If a window is closable, it
+ * typically shows an X in the upper right corner. Clicking on the X sends a
+ * close event to the server. Setting closable to false will remove the X
+ * from the window and prevent the user from closing the window.
+ *
+ * Note! For historical reasons readonly controls the closability of the
+ * window and therefore readonly and closable affect each other. Setting
+ * readonly to true will set closable to false and vice versa.
+ * <p/>
+ *
+ * @return true if the window can be closed by the user.
+ */
+ public boolean isClosable() {
+ return !isReadOnly();
+ }
+
+ /**
+ * Sets the closable status for the window. If a window is closable it
+ * typically shows an X in the upper right corner. Clicking on the X sends a
+ * close event to the server. Setting closable to false will remove the X
+ * from the window and prevent the user from closing the window.
+ *
+ * Note! For historical reasons readonly controls the closability of the
+ * window and therefore readonly and closable affect each other. Setting
+ * readonly to true will set closable to false and vice versa.
+ * <p/>
+ *
+ * @param closable
+ * determines if the window can be closed by the user.
+ */
+ public void setClosable(boolean closable) {
+ setReadOnly(!closable);
+ }
+
+ /**
+ * Indicates whether a window can be dragged or not. By default a window is
+ * draggable.
+ * <p/>
+ *
+ * @param draggable
+ * true if the window can be dragged by the user
+ */
+ public boolean isDraggable() {
+ return getState(false).draggable;
+ }
+
+ /**
+ * Enables or disables that a window can be dragged (moved) by the user. By
+ * default a window is draggable.
+ * <p/>
+ *
+ * @param draggable
+ * true if the window can be dragged by the user
+ */
+ public void setDraggable(boolean draggable) {
+ getState().draggable = draggable;
+ }
+
+ /**
+ * Gets the current mode of the window.
+ *
+ * @see WindowMode
+ * @return the mode of the window.
+ */
+ public WindowMode getWindowMode() {
+ return getState(false).windowMode;
+ }
+
+ /**
+ * Sets the mode for the window
+ *
+ * @see WindowMode
+ * @param windowMode
+ * The new mode
+ */
+ public void setWindowMode(WindowMode windowMode) {
+ if (windowMode != getWindowMode()) {
+ getState().windowMode = windowMode;
+ fireWindowWindowModeChange();
+ }
+ }
+
+ /**
+ * This is the old way of adding a keyboard shortcut to close a
+ * {@link Window} - to preserve compatibility with existing code under the
+ * new functionality, this method now first removes all registered close
+ * shortcuts, then adds the default ESCAPE shortcut key, and then attempts
+ * to add the shortcut provided as parameters to this method. This method,
+ * and its companion {@link #removeCloseShortcut()}, are now considered
+ * deprecated, as their main function is to preserve exact backwards
+ * compatibility with old code. For all new code, use the new keyboard
+ * shortcuts API: {@link #addCloseShortcut(int,int...)},
+ * {@link #removeCloseShortcut(int,int...)},
+ * {@link #removeAllCloseShortcuts()}, {@link #hasCloseShortcut(int,int...)}
+ * and {@link #getCloseShortcuts()}.
+ * <p>
+ * Original description: Makes it possible to close the window by pressing
+ * the given {@link KeyCode} and (optional) {@link ModifierKey}s.<br/>
+ * Note that this shortcut only reacts while the window has focus, closing
+ * itself - if you want to close a window from a UI, use
+ * {@link UI#addAction(com.vaadin.event.Action)} of the UI instead.
+ *
+ * @param keyCode
+ * the keycode for invoking the shortcut
+ * @param modifiers
+ * the (optional) modifiers for invoking the shortcut. Can be set
+ * to null to be explicit about not having modifiers.
+ *
+ * @deprecated Use {@link #addCloseShortcut(int, int...)} instead.
+ */
+ @Deprecated
+ public void setCloseShortcut(int keyCode, int... modifiers) {
+ removeCloseShortcut();
+ addCloseShortcut(keyCode, modifiers);
+ }
+
+ /**
+ * Removes all keyboard shortcuts previously set with
+ * {@link #setCloseShortcut(int, int...)} and
+ * {@link #addCloseShortcut(int, int...)}, then adds the default
+ * {@link KeyCode#ESCAPE} shortcut.
+ * <p>
+ * This is the old way of removing the (single) keyboard close shortcut, and
+ * is retained only for exact backwards compatibility. For all new code, use
+ * the new keyboard shortcuts API: {@link #addCloseShortcut(int,int...)},
+ * {@link #removeCloseShortcut(int,int...)},
+ * {@link #removeAllCloseShortcuts()}, {@link #hasCloseShortcut(int,int...)}
+ * and {@link #getCloseShortcuts()}.
+ *
+ * @deprecated Use {@link #removeCloseShortcut(int, int...)} instead.
+ */
+ @Deprecated
+ public void removeCloseShortcut() {
+ for (int i = 0; i < closeShortcuts.size(); ++i) {
+ CloseShortcut sc = closeShortcuts.get(i);
+ removeAction(sc);
+ }
+ closeShortcuts.clear();
+ addCloseShortcut(KeyCode.ESCAPE);
+ }
+
+ /**
+ * Adds a close shortcut - pressing this key while holding down all (if any)
+ * modifiers specified while this Window is in focus will close the Window.
+ *
+ * @since 7.6
+ * @param keyCode
+ * the keycode for invoking the shortcut
+ * @param modifiers
+ * the (optional) modifiers for invoking the shortcut. Can be set
+ * to null to be explicit about not having modifiers.
+ */
+ public void addCloseShortcut(int keyCode, int... modifiers) {
+
+ // Ignore attempts to re-add existing shortcuts
+ if (hasCloseShortcut(keyCode, modifiers)) {
+ return;
+ }
+
+ // Actually add the shortcut
+ CloseShortcut shortcut = new CloseShortcut(this, keyCode, modifiers);
+ addAction(shortcut);
+ closeShortcuts.add(shortcut);
+ }
+
+ /**
+ * Removes a close shortcut previously added with
+ * {@link #addCloseShortcut(int, int...)}.
+ *
+ * @since 7.6
+ * @param keyCode
+ * the keycode for invoking the shortcut
+ * @param modifiers
+ * the (optional) modifiers for invoking the shortcut. Can be set
+ * to null to be explicit about not having modifiers.
+ */
+ public void removeCloseShortcut(int keyCode, int... modifiers) {
+ for (CloseShortcut shortcut : closeShortcuts) {
+ if (shortcut.equals(keyCode, modifiers)) {
+ removeAction(shortcut);
+ closeShortcuts.remove(shortcut);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Removes all close shortcuts. This includes the default ESCAPE shortcut.
+ * It is up to the user to add back any and all keyboard close shortcuts
+ * they may require. For more fine-grained control over shortcuts, use
+ * {@link #removeCloseShortcut(int, int...)}.
+ *
+ * @since 7.6
+ */
+ public void removeAllCloseShortcuts() {
+ for (CloseShortcut shortcut : closeShortcuts) {
+ removeAction(shortcut);
+ }
+ closeShortcuts.clear();
+ }
+
+ /**
+ * Checks if a close window shortcut key has already been registered.
+ *
+ * @since 7.6
+ * @param keyCode
+ * the keycode for invoking the shortcut
+ * @param modifiers
+ * the (optional) modifiers for invoking the shortcut. Can be set
+ * to null to be explicit about not having modifiers.
+ * @return true, if an exactly matching shortcut has been registered.
+ */
+ public boolean hasCloseShortcut(int keyCode, int... modifiers) {
+ for (CloseShortcut shortcut : closeShortcuts) {
+ if (shortcut.equals(keyCode, modifiers)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns an unmodifiable collection of {@link CloseShortcut} objects
+ * currently registered with this {@link Window}. This method is provided
+ * mainly so that users can implement their own serialization routines. To
+ * check if a certain combination of keys has been registered as a close
+ * shortcut, use the {@link #hasCloseShortcut(int, int...)} method instead.
+ *
+ * @since 7.6
+ * @return an unmodifiable Collection of CloseShortcut objects.
+ */
+ public Collection<CloseShortcut> getCloseShortcuts() {
+ return Collections.unmodifiableCollection(closeShortcuts);
+ }
+
+ /**
+ * A {@link ShortcutListener} specifically made to define a keyboard
+ * shortcut that closes the window.
+ *
+ * <pre>
+ * <code>
+ * // within the window using helper
+ * window.setCloseShortcut(KeyCode.ESCAPE, null);
+ *
+ * // or globally
+ * getUI().addAction(new Window.CloseShortcut(window, KeyCode.ESCAPE));
+ * </code>
+ * </pre>
+ *
+ */
+ public static class CloseShortcut extends ShortcutListener {
+ protected Window window;
+
+ /**
+ * Creates a keyboard shortcut for closing the given window using the
+ * shorthand notation defined in {@link ShortcutAction}.
+ *
+ * @param window
+ * to be closed when the shortcut is invoked
+ * @param shorthandCaption
+ * the caption with shortcut keycode and modifiers indicated
+ */
+ public CloseShortcut(Window window, String shorthandCaption) {
+ super(shorthandCaption);
+ this.window = window;
+ }
+
+ /**
+ * Creates a keyboard shortcut for closing the given window using the
+ * given {@link KeyCode} and {@link ModifierKey}s.
+ *
+ * @param window
+ * to be closed when the shortcut is invoked
+ * @param keyCode
+ * KeyCode to react to
+ * @param modifiers
+ * optional modifiers for shortcut
+ */
+ public CloseShortcut(Window window, int keyCode, int... modifiers) {
+ super(null, keyCode, modifiers);
+ this.window = window;
+ }
+
+ /**
+ * Creates a keyboard shortcut for closing the given window using the
+ * given {@link KeyCode}.
+ *
+ * @param window
+ * to be closed when the shortcut is invoked
+ * @param keyCode
+ * KeyCode to react to
+ */
+ public CloseShortcut(Window window, int keyCode) {
+ this(window, keyCode, null);
+ }
+
+ @Override
+ public void handleAction(Object sender, Object target) {
++ if (window.isClosable()) {
++ window.close();
++ }
+ }
+
+ public boolean equals(int keyCode, int... modifiers) {
+ if (keyCode != getKeyCode()) {
+ return false;
+ }
+
+ if (getModifiers() != null) {
+ int[] mods = null;
+ if (modifiers != null) {
+ // Modifiers provided by the parent ShortcutAction class
+ // are guaranteed to be sorted. We still need to sort
+ // the modifiers passed in as argument.
+ mods = Arrays.copyOf(modifiers, modifiers.length);
+ Arrays.sort(mods);
+ }
+ return Arrays.equals(mods, getModifiers());
+ }
+ return true;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.FieldEvents.FocusNotifier#addFocusListener(com.vaadin
+ * .event.FieldEvents.FocusListener)
+ */
+ @Override
+ public void addFocusListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addFocusListener(FocusListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(FocusListener listener) {
+ addFocusListener(listener);
+ }
+
+ @Override
+ public void removeFocusListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeFocusListener(FocusListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(FocusListener listener) {
+ removeFocusListener(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.FieldEvents.BlurNotifier#addBlurListener(com.vaadin.
+ * event.FieldEvents.BlurListener)
+ */
+ @Override
+ public void addBlurListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by {@link #addBlurListener(BlurListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(BlurListener listener) {
+ addBlurListener(listener);
+ }
+
+ @Override
+ public void removeBlurListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeBlurListener(BlurListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(BlurListener listener) {
+ removeBlurListener(listener);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Cause the window to be brought on top of other windows and gain keyboard
+ * focus.
+ */
+ @Override
+ public void focus() {
+ /*
+ * When focusing a window it basically means it should be brought to the
+ * front. Instead of just moving the keyboard focus we focus the window
+ * and bring it top-most.
+ */
+ super.focus();
+ bringToFront();
+ }
+
+ @Override
+ protected WindowState getState() {
+ return (WindowState) super.getState();
+ }
+
+ @Override
+ protected WindowState getState(boolean markAsDirty) {
+ return (WindowState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Allows to specify which components contain the description for the
+ * window. Text contained in these components will be read by assistive
+ * devices when it is opened.
+ *
+ * @param components
+ * the components to use as description
+ */
+ public void setAssistiveDescription(Component... components) {
+ if (components == null) {
+ throw new IllegalArgumentException(
+ "Parameter connectors must be non-null");
+ } else {
+ getState().contentDescription = components;
+ }
+ }
+
+ /**
+ * Gets the components that are used as assistive description. Text
+ * contained in these components will be read by assistive devices when the
+ * window is opened.
+ *
+ * @return array of previously set components
+ */
+ public Component[] getAssistiveDescription() {
+ Connector[] contentDescription = getState(false).contentDescription;
+ if (contentDescription == null) {
+ return null;
+ }
+
+ Component[] target = new Component[contentDescription.length];
+ System.arraycopy(contentDescription, 0, target, 0,
+ contentDescription.length);
+
+ return target;
+ }
+
+ /**
+ * Sets the accessibility prefix for the window caption.
+ *
+ * This prefix is read to assistive device users before the window caption,
+ * but not visible on the page.
+ *
+ * @param prefix
+ * String that is placed before the window caption
+ */
+ public void setAssistivePrefix(String prefix) {
+ getState().assistivePrefix = prefix;
+ }
+
+ /**
+ * Gets the accessibility prefix for the window caption.
+ *
+ * This prefix is read to assistive device users before the window caption,
+ * but not visible on the page.
+ *
+ * @return The accessibility prefix
+ */
+ public String getAssistivePrefix() {
+ return getState(false).assistivePrefix;
+ }
+
+ /**
+ * Sets the accessibility postfix for the window caption.
+ *
+ * This postfix is read to assistive device users after the window caption,
+ * but not visible on the page.
+ *
+ * @param prefix
+ * String that is placed after the window caption
+ */
+ public void setAssistivePostfix(String assistivePostfix) {
+ getState().assistivePostfix = assistivePostfix;
+ }
+
+ /**
+ * Gets the accessibility postfix for the window caption.
+ *
+ * This postfix is read to assistive device users after the window caption,
+ * but not visible on the page.
+ *
+ * @return The accessibility postfix
+ */
+ public String getAssistivePostfix() {
+ return getState(false).assistivePostfix;
+ }
+
+ /**
+ * Sets the WAI-ARIA role the window.
+ *
+ * This role defines how an assistive device handles a window. Available
+ * roles are alertdialog and dialog (@see <a
+ * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles
+ * Model</a>).
+ *
+ * The default role is dialog.
+ *
+ * @param role
+ * WAI-ARIA role to set for the window
+ */
+ public void setAssistiveRole(WindowRole role) {
+ getState().role = role;
+ }
+
+ /**
+ * Gets the WAI-ARIA role the window.
+ *
+ * This role defines how an assistive device handles a window. Available
+ * roles are alertdialog and dialog (@see <a
+ * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles
+ * Model</a>).
+ *
+ * @return WAI-ARIA role set for the window
+ */
+ public WindowRole getAssistiveRole() {
+ return getState(false).role;
+ }
+
+ /**
+ * Set if it should be prevented to set the focus to a component outside a
+ * non-modal window with the tab key.
+ * <p>
+ * This is meant to help users of assistive devices to not leaving the
+ * window unintentionally.
+ * <p>
+ * For modal windows, this function is activated automatically, while
+ * preserving the stored value of tabStop.
+ *
+ * @param tabStop
+ * true to keep the focus inside the window when reaching the top
+ * or bottom, false (default) to allow leaving the window
+ */
+ public void setTabStopEnabled(boolean tabStop) {
+ getState().assistiveTabStop = tabStop;
+ }
+
+ /**
+ * Get if it is prevented to leave a window with the tab key.
+ *
+ * @return true when the focus is limited to inside the window, false when
+ * focus can leave the window
+ */
+ public boolean isTabStopEnabled() {
+ return getState(false).assistiveTabStop;
+ }
+
+ /**
+ * Sets the message that is provided to users of assistive devices when the
+ * user reaches the top of the window when leaving a window with the tab key
+ * is prevented.
+ * <p>
+ * This message is not visible on the screen.
+ *
+ * @param topMessage
+ * String provided when the user navigates with Shift-Tab keys to
+ * the top of the window
+ */
+ public void setTabStopTopAssistiveText(String topMessage) {
+ getState().assistiveTabStopTopText = topMessage;
+ }
+
+ /**
+ * Sets the message that is provided to users of assistive devices when the
+ * user reaches the bottom of the window when leaving a window with the tab
+ * key is prevented.
+ * <p>
+ * This message is not visible on the screen.
+ *
+ * @param bottomMessage
+ * String provided when the user navigates with the Tab key to
+ * the bottom of the window
+ */
+ public void setTabStopBottomAssistiveText(String bottomMessage) {
+ getState().assistiveTabStopBottomText = bottomMessage;
+ }
+
+ /**
+ * Gets the message that is provided to users of assistive devices when the
+ * user reaches the top of the window when leaving a window with the tab key
+ * is prevented.
+ *
+ * @return the top message
+ */
+ public String getTabStopTopAssistiveText() {
+ return getState(false).assistiveTabStopTopText;
+ }
+
+ /**
+ * Gets the message that is provided to users of assistive devices when the
+ * user reaches the bottom of the window when leaving a window with the tab
+ * key is prevented.
+ *
+ * @return the bottom message
+ */
+ public String getTabStopBottomAssistiveText() {
+ return getState(false).assistiveTabStopBottomText;
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext context) {
+ super.readDesign(design, context);
+
+ if (design.hasAttr("center")) {
+ center();
+ }
+ if (design.hasAttr("position")) {
+ String[] position = design.attr("position").split(",");
+ setPositionX(Integer.parseInt(position[0]));
+ setPositionY(Integer.parseInt(position[1]));
+ }
+
+ // Parse shortcuts if defined, otherwise rely on default behavior
+ if (design.hasAttr("close-shortcut")) {
+
+ // Parse shortcuts
+ String[] shortcutStrings = DesignAttributeHandler.readAttribute(
+ "close-shortcut", design.attributes(), String.class).split(
+ "\\s+");
+
+ removeAllCloseShortcuts();
+
+ for (String part : shortcutStrings) {
+ if (!part.isEmpty()) {
+ ShortcutAction shortcut = DesignAttributeHandler
+ .getFormatter().parse(part.trim(),
+ ShortcutAction.class);
+ addCloseShortcut(shortcut.getKeyCode(),
+ shortcut.getModifiers());
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads the content and possible assistive descriptions from the list of
+ * child elements of a design. If an element has an
+ * {@code :assistive-description} attribute, adds the parsed component to
+ * the list of components used as the assistive description of this Window.
+ * Otherwise, sets the component as the content of this Window. If there are
+ * multiple non-description elements, throws a DesignException.
+ *
+ * @param children
+ * child elements in a design
+ * @param context
+ * the DesignContext instance used to parse the design
+ *
+ * @throws DesignException
+ * if there are multiple non-description child elements
+ * @throws DesignException
+ * if a child element could not be parsed as a Component
+ *
+ * @see #setContent(Component)
+ * @see #setAssistiveDescription(Component...)
+ */
+ @Override
+ protected void readDesignChildren(Elements children, DesignContext context) {
+ List<Component> descriptions = new ArrayList<Component>();
+ Elements content = new Elements();
+
+ for (Element child : children) {
+ if (child.hasAttr(":assistive-description")) {
+ descriptions.add(context.readDesign(child));
+ } else {
+ content.add(child);
+ }
+ }
+ super.readDesignChildren(content, context);
+ setAssistiveDescription(descriptions.toArray(new Component[0]));
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext context) {
+ super.writeDesign(design, context);
+
+ Window def = context.getDefaultInstance(this);
+
+ if (getState().centered) {
+ design.attr("center", true);
+ }
+
+ DesignAttributeHandler.writeAttribute("position", design.attributes(),
+ getPosition(), def.getPosition(), String.class);
+
+ // Process keyboard shortcuts
+ if (closeShortcuts.size() == 1 && hasCloseShortcut(KeyCode.ESCAPE)) {
+ // By default, we won't write anything if we're relying on default
+ // shortcut behavior
+ } else {
+ // Dump all close shortcuts to a string...
+ String attrString = "";
+
+ // TODO: add canonical support for array data in XML attributes
+ for (CloseShortcut shortcut : closeShortcuts) {
+ String shortcutString = DesignAttributeHandler.getFormatter()
+ .format(shortcut, CloseShortcut.class);
+ attrString += shortcutString + " ";
+ }
+
+ // Write everything except the last "," to the design
+ DesignAttributeHandler.writeAttribute("close-shortcut",
+ design.attributes(), attrString.trim(), null, String.class);
+ }
+
+ for (Component c : getAssistiveDescription()) {
+ Element child = context.createElement(c).attr(
+ ":assistive-description", true);
+ design.appendChild(child);
+ }
+ }
+
+ private String getPosition() {
+ return getPositionX() + "," + getPositionY();
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add("center");
+ result.add("position");
+ result.add("position-y");
+ result.add("position-x");
+ result.add("close-shortcut");
+ return result;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2014 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.ui.declarative;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.jsoup.nodes.Attribute;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+
+import com.vaadin.data.util.converter.Converter;
++import com.vaadin.shared.ui.AlignmentInfo;
+import com.vaadin.shared.util.SharedUtil;
++import com.vaadin.ui.Alignment;
+
+/**
+ * Default attribute handler implementation used when parsing designs to
+ * component trees. Handles all the component attributes that do not require
+ * custom handling.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignAttributeHandler implements Serializable {
+
+ private static Logger getLogger() {
+ return Logger.getLogger(DesignAttributeHandler.class.getName());
+ }
+
+ private static Map<Class<?>, AttributeCacheEntry> cache = new ConcurrentHashMap<Class<?>, AttributeCacheEntry>();
+
+ // translates string <-> object
+ private static DesignFormatter FORMATTER = new DesignFormatter();
+
+ /**
+ * Returns the currently used formatter. All primitive types and all types
+ * needed by Vaadin components are handled by that formatter.
+ *
+ * @return An instance of the formatter.
+ */
+ public static DesignFormatter getFormatter() {
+ return FORMATTER;
+ }
+
+ /**
+ * Clears the children and attributes of the given element
+ *
+ * @param design
+ * the element to be cleared
+ */
+ public static void clearElement(Element design) {
+ Attributes attr = design.attributes();
+ for (Attribute a : attr.asList()) {
+ attr.remove(a.getKey());
+ }
+ List<Node> children = new ArrayList<Node>();
+ children.addAll(design.childNodes());
+ for (Node node : children) {
+ node.remove();
+ }
+ }
+
+ /**
+ * Assigns the specified design attribute to the given component.
+ *
+ * @param target
+ * the target to which the attribute should be set
+ * @param attribute
+ * the name of the attribute to be set
+ * @param value
+ * the string value of the attribute
+ * @return true on success
+ */
+ public static boolean assignValue(Object target, String attribute,
+ String value) {
+ if (target == null || attribute == null || value == null) {
+ throw new IllegalArgumentException(
+ "Parameters with null value not allowed");
+ }
+ boolean success = false;
+ try {
+ Method setter = findSetterForAttribute(target.getClass(), attribute);
+ if (setter == null) {
+ // if we don't have the setter, there is no point in continuing
+ success = false;
+ } else {
+ // we have a value from design attributes, let's use that
+ Object param = getFormatter().parse(value,
+ setter.getParameterTypes()[0]);
+ setter.invoke(target, param);
+ success = true;
+ }
+ } catch (Exception e) {
+ getLogger().log(
+ Level.WARNING,
+ "Failed to set value \"" + value + "\" to attribute "
+ + attribute, e);
+ }
+ if (!success) {
+ getLogger().info(
+ "property " + attribute
+ + " ignored by default attribute handler");
+ }
+ return success;
+ }
+
+ /**
+ * Searches for supported setter and getter types from the specified class
+ * and returns the list of corresponding design attributes
+ *
+ * @param clazz
+ * the class scanned for setters
+ * @return the list of supported design attributes
+ */
+ public static Collection<String> getSupportedAttributes(Class<?> clazz) {
+ resolveSupportedAttributes(clazz);
+ return cache.get(clazz).getAttributes();
+ }
+
+ /**
+ * Resolves the supported attributes and corresponding getters and setters
+ * for the class using introspection. After resolving, the information is
+ * cached internally by this class
+ *
+ * @param clazz
+ * the class to resolve the supported attributes for
+ */
+ private static void resolveSupportedAttributes(Class<?> clazz) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("The clazz can not be null");
+ }
+ if (cache.containsKey(clazz)) {
+ // NO-OP
+ return;
+ }
+ BeanInfo beanInfo;
+ try {
+ beanInfo = Introspector.getBeanInfo(clazz);
+ } catch (IntrospectionException e) {
+ throw new RuntimeException(
+ "Could not get supported attributes for class "
+ + clazz.getName());
+ }
+ AttributeCacheEntry entry = new AttributeCacheEntry();
+ for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
+ Method getter = descriptor.getReadMethod();
+ Method setter = descriptor.getWriteMethod();
+ if (getter != null && setter != null
+ && getFormatter().canConvert(descriptor.getPropertyType())) {
+ String attribute = toAttributeName(descriptor.getName());
+ entry.addAttribute(attribute, getter, setter);
+ }
+ }
+ cache.put(clazz, entry);
+ }
+
+ /**
+ * Writes the specified attribute to the design if it differs from the
+ * default value got from the <code> defaultInstance <code>
+ *
+ * @param component
+ * the component used to get the attribute value
+ * @param attribute
+ * the key for the attribute
+ * @param attr
+ * the attribute list where the attribute will be written
+ * @param defaultInstance
+ * the default instance for comparing default values
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public static void writeAttribute(Object component, String attribute,
+ Attributes attr, Object defaultInstance) {
+ Method getter = findGetterForAttribute(component.getClass(), attribute);
+ if (getter == null) {
+ getLogger().warning(
+ "Could not find getter for attribute " + attribute);
+ } else {
+ try {
+ // compare the value with default value
+ Object value = getter.invoke(component);
+ Object defaultValue = getter.invoke(defaultInstance);
+ writeAttribute(attribute, attr, value, defaultValue,
+ (Class) getter.getReturnType());
+ } catch (Exception e) {
+ getLogger()
+ .log(Level.SEVERE,
+ "Failed to invoke getter for attribute "
+ + attribute, e);
+ }
+ }
+ }
+
+ /**
+ * Writes the given attribute value to a set of attributes if it differs
+ * from the default attribute value.
+ *
+ * @param attribute
+ * the attribute key
+ * @param attributes
+ * the set of attributes where the new attribute is written
+ * @param value
+ * the attribute value
+ * @param defaultValue
+ * the default attribute value
+ * @param inputType
+ * the type of the input value
+ */
+ public static <T> void writeAttribute(String attribute,
+ Attributes attributes, T value, T defaultValue, Class<T> inputType) {
+ if (!getFormatter().canConvert(inputType)) {
+ throw new IllegalArgumentException("input type: "
+ + inputType.getName() + " not supported");
+ }
+ if (!SharedUtil.equals(value, defaultValue)) {
+ String attributeValue = toAttributeValue(inputType, value);
+ if ("".equals(attributeValue)
+ && (inputType == boolean.class || inputType == Boolean.class)) {
+ attributes.put(attribute, true);
+ } else {
+ attributes.put(attribute, attributeValue);
+ }
+ }
+ }
+
+ /**
+ * Reads the given attribute from a set of attributes. If attribute does not
+ * exist return a given default value.
+ *
+ * @param attribute
+ * the attribute key
+ * @param attributes
+ * the set of attributes to read from
+ * @param defaultValue
+ * the default value to return if attribute does not exist
+ * @param outputType
+ * the output type for the attribute
+ * @return the attribute value or the default value if the attribute is not
+ * found
+ */
+ public static <T> T readAttribute(String attribute, Attributes attributes,
+ T defaultValue, Class<T> outputType) {
+ T value = readAttribute(attribute, attributes, outputType);
+ if (value != null) {
+ return value;
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Reads the given attribute from a set of attributes.
+ *
+ * @param attribute
+ * the attribute key
+ * @param attributes
+ * the set of attributes to read from
+ * @param outputType
+ * the output type for the attribute
+ * @return the attribute value or null
+ */
+ public static <T> T readAttribute(String attribute, Attributes attributes,
+ Class<T> outputType) {
+ if (!getFormatter().canConvert(outputType)) {
+ throw new IllegalArgumentException("output type: "
+ + outputType.getName() + " not supported");
+ }
+ if (!attributes.hasKey(attribute)) {
+ return null;
+ } else {
+ try {
+ String value = attributes.get(attribute);
+ return getFormatter().parse(value, outputType);
+ } catch (Exception e) {
+ throw new DesignException("Failed to read attribute "
+ + attribute, e);
+ }
+ }
+ }
+
+ /**
+ * Returns the design attribute name corresponding the given method name.
+ * For example given a method name <code>setPrimaryStyleName</code> the
+ * return value would be <code>primary-style-name</code>
+ *
+ * @param propertyName
+ * the property name returned by {@link IntroSpector}
+ * @return the design attribute name corresponding the given method name
+ */
+ private static String toAttributeName(String propertyName) {
+ propertyName = removeSubsequentUppercase(propertyName);
+ String[] words = propertyName.split("(?<!^)(?=[A-Z])");
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < words.length; i++) {
+ if (builder.length() > 0) {
+ builder.append("-");
+ }
+ builder.append(words[i].toLowerCase());
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Replaces subsequent UPPERCASE strings of length 2 or more followed either
+ * by another uppercase letter or an end of string. This is to generalise
+ * handling of method names like <tt>showISOWeekNumbers</tt>.
+ *
+ * @param param
+ * Input string.
+ * @return Input string with sequences of UPPERCASE turned into Normalcase.
+ */
+ private static String removeSubsequentUppercase(String param) {
+ StringBuffer result = new StringBuffer();
+ // match all two-or-more caps letters lead by a non-uppercase letter
+ // followed by either a capital letter or string end
+ Pattern pattern = Pattern.compile("(^|[^A-Z])([A-Z]{2,})([A-Z]|$)");
+ Matcher matcher = pattern.matcher(param);
+ while (matcher.find()) {
+ String matched = matcher.group(2);
+ // if this is a beginning of the string, the whole matched group is
+ // written in lower case
+ if (matcher.group(1).isEmpty()) {
+ matcher.appendReplacement(result, matched.toLowerCase()
+ + matcher.group(3));
+ // otherwise the first character of the group stays uppercase,
+ // while the others are lower case
+ } else {
+ matcher.appendReplacement(
+ result,
+ matcher.group(1) + matched.substring(0, 1)
+ + matched.substring(1).toLowerCase()
+ + matcher.group(3));
+ }
+ // in both cases the uppercase letter of the next word (or string's
+ // end) is added
+ // this implies there is at least one extra lowercase letter after
+ // it to be caught by the next call to find()
+ }
+ matcher.appendTail(result);
+ return result.toString();
+ }
+
+ /**
+ * Serializes the given value to valid design attribute representation
+ *
+ * @param sourceType
+ * the type of the value
+ * @param value
+ * the value to be serialized
+ * @return the given value as design attribute representation
+ */
+ private static String toAttributeValue(Class<?> sourceType, Object value) {
+ if (value == null) {
+ // TODO: Handle corner case where sourceType is String and default
+ // value is not null. How to represent null value in attributes?
+ return "";
+ }
+ Converter<String, Object> converter = getFormatter().findConverterFor(
+ sourceType);
+ if (converter != null) {
+ return converter.convertToPresentation(value, String.class, null);
+ } else {
+ return value.toString();
+ }
+ }
+
+ /**
+ * Returns a setter that can be used for assigning the given design
+ * attribute to the class
+ *
+ * @param clazz
+ * the class that is scanned for setters
+ * @param attribute
+ * the design attribute to find setter for
+ * @return the setter method or null if not found
+ */
+ private static Method findSetterForAttribute(Class<?> clazz,
+ String attribute) {
+ resolveSupportedAttributes(clazz);
+ return cache.get(clazz).getSetter(attribute);
+ }
+
+ /**
+ * Returns a getter that can be used for reading the given design attribute
+ * value from the class
+ *
+ * @param clazz
+ * the class that is scanned for getters
+ * @param attribute
+ * the design attribute to find getter for
+ * @return the getter method or null if not found
+ */
+ private static Method findGetterForAttribute(Class<?> clazz,
+ String attribute) {
+ resolveSupportedAttributes(clazz);
+ return cache.get(clazz).getGetter(attribute);
+ }
+
+ /**
+ * Cache object for caching supported attributes and their getters and
+ * setters
+ *
+ * @author Vaadin Ltd
+ */
+ private static class AttributeCacheEntry implements Serializable {
+ private Map<String, Method[]> accessMethods = new ConcurrentHashMap<String, Method[]>();
+
+ private void addAttribute(String attribute, Method getter, Method setter) {
+ Method[] methods = new Method[2];
+ methods[0] = getter;
+ methods[1] = setter;
+ accessMethods.put(attribute, methods);
+ }
+
+ private Collection<String> getAttributes() {
+ ArrayList<String> attributes = new ArrayList<String>();
+ attributes.addAll(accessMethods.keySet());
+ return attributes;
+ }
+
+ private Method getGetter(String attribute) {
+ Method[] methods = accessMethods.get(attribute);
+ return (methods != null && methods.length > 0) ? methods[0] : null;
+ }
+
+ private Method getSetter(String attribute) {
+ Method[] methods = accessMethods.get(attribute);
+ return (methods != null && methods.length > 1) ? methods[1] : null;
+ }
+ }
++
++ /**
++ * Read the alignment from the given child component attributes.
++ *
++ * @since 7.6.4
++ * @param attr
++ * the child component attributes
++ * @return the component alignment
++ */
++ public static Alignment readAlignment(Attributes attr) {
++ int bitMask = 0;
++ if (attr.hasKey(":middle")) {
++ bitMask += AlignmentInfo.Bits.ALIGNMENT_VERTICAL_CENTER;
++ } else if (attr.hasKey(":bottom")) {
++ bitMask += AlignmentInfo.Bits.ALIGNMENT_BOTTOM;
++ } else {
++ bitMask += AlignmentInfo.Bits.ALIGNMENT_TOP;
++ }
++ if (attr.hasKey(":center")) {
++ bitMask += AlignmentInfo.Bits.ALIGNMENT_HORIZONTAL_CENTER;
++ } else if (attr.hasKey(":right")) {
++ bitMask += AlignmentInfo.Bits.ALIGNMENT_RIGHT;
++ } else {
++ bitMask += AlignmentInfo.Bits.ALIGNMENT_LEFT;
++ }
++
++ return new Alignment(bitMask);
++ }
++
++ /**
++ * Writes the alignment to the given child element attributes.
++ *
++ * @since 7.6.4
++ * @param childElement
++ * the child element
++ * @param alignment
++ * the component alignment
++ */
++ public static void writeAlignment(Element childElement, Alignment alignment) {
++ if (alignment.isMiddle()) {
++ childElement.attr(":middle", true);
++ } else if (alignment.isBottom()) {
++ childElement.attr(":bottom", true);
++ }
++ if (alignment.isCenter()) {
++ childElement.attr(":center", true);
++ } else if (alignment.isRight()) {
++ childElement.attr(":right", true);
++ }
++ }
++
+}
--- /dev/null
--- /dev/null
++/*
++ * Copyright 2000-2014 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.server;
++
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++
++import org.junit.Assert;
++import org.junit.Test;
++
++import com.vaadin.annotations.Theme;
++import com.vaadin.annotations.Widgetset;
++
++/**
++ * Tests for {@link UIProvider} class.
++ *
++ * @author Vaadin Ltd
++ */
++public class UIProviderTest {
++
++ @Test
++ public void getAnnotationFor_widgetsetAnnotationForSubclass_annotationFound() {
++ Assert.assertNotNull("Widgetset annotation is not found for subclass",
++ UIProvider.getAnnotationFor(TestClass.class, Widgetset.class));
++ }
++
++ @Test
++ public void getAnnotationFor_themeAnnotationForSubclass_annotationFound() {
++ Assert.assertNotNull("Theme annotation is not found for subclass",
++ UIProvider.getAnnotationFor(TestClass.class, Theme.class));
++ }
++
++ @Test
++ public void getAnnotationFor_themeAnnotationForSubclass_annotationOverridden() {
++ Assert.assertEquals(
++ "Theme annotation is not overridden correctly in subclass",
++ "c", UIProvider.getAnnotationFor(TestClass.class, Theme.class)
++ .value());
++ }
++
++ @Test
++ public void getAnnotationFor_notInheritedAnnotationForSubclass_annotationFound() {
++ Assert.assertNotNull(
++ "TestAnnotation annotation is not found for subclass",
++ UIProvider.getAnnotationFor(TestClass.class,
++ TestAnnotation.class));
++ }
++
++ @Test
++ public void getAnnotationFor_directAnnotationForSubclass_annotationFound() {
++ Assert.assertNotNull(
++ "TestAnnotation1 annotation is not found for subclass",
++ UIProvider.getAnnotationFor(TestClass.class,
++ TestAnnotation1.class));
++ }
++
++ @Test
++ public void getAnnotationFor_annotationInheritedFromInterface_annotationFound() {
++ Assert.assertNotNull(
++ "Theme annotation is not inherited from interface", UIProvider
++ .getAnnotationFor(ClassImplementingInterface.class,
++ Theme.class));
++ }
++
++ @Retention(RetentionPolicy.RUNTIME)
++ @Target(ElementType.TYPE)
++ public @interface TestAnnotation {
++
++ }
++
++ @Retention(RetentionPolicy.RUNTIME)
++ @Target(ElementType.TYPE)
++ public @interface TestAnnotation1 {
++
++ }
++
++ @Widgetset("a")
++ @Theme("b")
++ @TestAnnotation
++ public static class TestSuperClass {
++
++ }
++
++ @TestAnnotation1
++ @Theme("c")
++ public static class TestClass extends TestSuperClass {
++
++ }
++
++ @Theme("d")
++ public interface InterfaceWithAnnotation {
++ }
++
++ public static class ClassImplementingInterface implements
++ InterfaceWithAnnotation {
++ }
++
++}
--- /dev/null
- Button b1 = new Button("Button 0,0 -> 1,1");
+/*
+ * Copyright 2000-2014 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.tests.server.component.gridlayout;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.shared.ui.label.ContentMode;
+import com.vaadin.tests.server.component.DeclarativeMarginTestBase;
+import com.vaadin.ui.Alignment;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.GridLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.declarative.Design;
+import com.vaadin.ui.declarative.DesignContext;
+
+public class GridLayoutDeclarativeTest extends
+ DeclarativeMarginTestBase<GridLayout> {
+
+ @Test
+ public void testMargins() {
+ testMargins("vaadin-grid-layout");
+ }
+
+ @Test
+ public void testSimpleGridLayout() {
+ Button b1 = new Button("Button 0,0");
+ Button b2 = new Button("Button 0,1");
+ Button b3 = new Button("Button 1,0");
+ Button b4 = new Button("Button 1,1");
+ b1.setCaptionAsHtml(true);
+ b2.setCaptionAsHtml(true);
+ b3.setCaptionAsHtml(true);
+ b4.setCaptionAsHtml(true);
+ String design = "<vaadin-grid-layout><row>" //
+ + "<column expand=1>" + writeChild(b1) + "</column>" //
+ + "<column expand=3>" + writeChild(b2) + "</column>" //
+ + "</row><row>" //
+ + "<column>" + writeChild(b3) + "</column>" //
+ + "<column>" + writeChild(b4) + "</column>" //
+ + "</row></vaadin-grid-layout>";
+ GridLayout gl = new GridLayout(2, 2);
+ gl.addComponent(b1);
+ gl.addComponent(b2);
+ gl.addComponent(b3);
+ gl.addComponent(b4);
+ gl.setColumnExpandRatio(0, 1.0f);
+ gl.setColumnExpandRatio(1, 3.0f);
+ testWrite(design, gl);
+ testRead(design, gl);
+ }
+
+ @Test
+ public void testOneBigComponentGridLayout() {
- Button b1 = new Button("Button 0,0 -> 0,2");
++ Button b1 = new Button("Button 0,0 -> 1,1");
+ b1.setCaptionAsHtml(true);
+ String design = "<vaadin-grid-layout><row>" //
+ + "<column colspan=2 rowspan=2>" + writeChild(b1) + "</column>" //
+ + "</row><row expand=2>" //
+ + "</row></vaadin-grid-layout>";
+ GridLayout gl = new GridLayout(2, 2);
+ gl.addComponent(b1, 0, 0, 1, 1);
+ gl.setRowExpandRatio(1, 2);
+ testWrite(design, gl);
+ testRead(design, gl);
+ }
+
+ @Test
+ public void testMultipleSpannedComponentsGridLayout() {
+ GridLayout gl = new GridLayout(5, 5);
- Button b2 = new Button("Button 0,3 -> 3,3");
++ Button b1 = new Button("Button 0,0 -> 0,2");
+ b1.setCaptionAsHtml(true);
+ gl.addComponent(b1, 0, 0, 2, 0);
+
- Button b3 = new Button("Button 0,4 -> 1,4");
++ Button b2 = new Button("Button 0,3 -> 3,3");
+ b2.setCaptionAsHtml(true);
+ gl.addComponent(b2, 3, 0, 3, 3);
+
- Button b4 = new Button("Button 1,0 -> 3,1");
++ Button b3 = new Button("Button 0,4 -> 1,4");
+ b3.setCaptionAsHtml(true);
+ gl.addComponent(b3, 4, 0, 4, 1);
+
- Button b6 = new Button("Button 3,4 -> 4,4");
++ Button b4 = new Button("Button 1,0 -> 3,1");
+ b4.setCaptionAsHtml(true);
+ gl.addComponent(b4, 0, 1, 1, 3);
+
+ Button b5 = new Button("Button 2,2");
+ b5.setCaptionAsHtml(true);
+ gl.addComponent(b5, 2, 2);
+
- Button b7 = new Button("Button 4,1 -> 4,2");
++ Button b6 = new Button("Button 3,4 -> 4,4");
+ b6.setCaptionAsHtml(true);
+ gl.addComponent(b6, 4, 3, 4, 4);
+
- Button b1 = new Button("Button 0,4 -> 4,4");
++ Button b7 = new Button("Button 4,1 -> 4,2");
+ b7.setCaptionAsHtml(true);
+ gl.addComponent(b7, 2, 4, 3, 4);
+
+ /*
+ * Buttons in the GridLayout
+ */
+
+ // 1 1 1 2 3
+ // 4 4 - 2 3
+ // 4 4 5 2 -
+ // 4 4 - 2 6
+ // - - 7 7 6
+
+ String design = "<vaadin-grid-layout><row>" //
+ + "<column colspan=3>" + writeChild(b1) + "</column>" //
+ + "<column rowspan=4>" + writeChild(b2) + "</column>" //
+ + "<column rowspan=2>" + writeChild(b3) + "</column>" //
+ + "</row><row>" //
+ + "<column rowspan=3 colspan=2>" + writeChild(b4) + "</column>" //
+ + "</row><row>" //
+ + "<column>" + writeChild(b5) + "</column>" //
+ + "</row><row>" //
+ + "<column />" // Empty placeholder
+ + "<column rowspan=2>" + writeChild(b6) + "</column>" //
+ + "</row><row>" //
+ + "<column colspan=2 />" // Empty placeholder
+ + "<column colspan=2>" + writeChild(b7) + "</column>" //
+ + "</row></vaadin-grid-layout>";
+ testWrite(design, gl);
+ testRead(design, gl);
+ }
+
+ @Test
+ public void testManyExtraGridLayoutSlots() {
+ GridLayout gl = new GridLayout(5, 5);
- Button b1 = new Button("Button 0,4 -> 4,4");
++ Button b1 = new Button("Button 0,4 -> 4,4");
+ b1.setCaptionAsHtml(true);
+ gl.addComponent(b1, 4, 0, 4, 4);
+ gl.setColumnExpandRatio(2, 2.0f);
+
+ String design = "<vaadin-grid-layout><row>" //
+ + "<column colspan=4 rowspan=5 expand='0,0,2,0' />" //
+ + "<column rowspan=5>" + writeChild(b1) + "</column>" //
+ + "</row><row>" //
+ + "</row><row>" //
+ + "</row><row>" //
+ + "</row><row>" //
+ + "</row></vaadin-grid-layout>";
+ testWrite(design, gl);
+ testRead(design, gl);
+ }
+
+ @Test
+ public void testManyEmptyColumnsWithOneExpand() {
+ GridLayout gl = new GridLayout(5, 5);
- Assert.assertTrue(Math.abs(expected.getRowExpandRatio(row)
- - result.getRowExpandRatio(row)) < 0.00001);
++ Button b1 = new Button("Button 0,4 -> 4,4");
+ b1.setCaptionAsHtml(true);
+ gl.addComponent(b1, 0, 0, 0, 4);
+ gl.setColumnExpandRatio(4, 2.0f);
+
+ String design = "<vaadin-grid-layout><row>" //
+ + "<column rowspan=5>" + writeChild(b1) + "</column>" //
+ + "<column colspan=4 rowspan=5 expand='0,0,0,2' />" //
+ + "</row><row>" //
+ + "</row><row>" //
+ + "</row><row>" //
+ + "</row><row>" //
+ + "</row></vaadin-grid-layout>";
+ testWrite(design, gl);
+ testRead(design, gl);
+ }
+
+ @Test
+ public void testEmptyGridLayout() {
+ GridLayout gl = new GridLayout();
+ String design = "<vaadin-grid-layout />";
+ testWrite(design, gl);
+ testRead(design, gl);
+ }
+
+ private String writeChild(Component childComponent) {
+ return new DesignContext().createElement(childComponent).toString();
+ }
+
+ @Override
+ public GridLayout testRead(String design, GridLayout expected) {
+ expected.setCursorX(0);
+ expected.setCursorY(expected.getRows());
+
+ GridLayout result = super.testRead(design, expected);
+ for (int row = 0; row < expected.getRows(); ++row) {
- Assert.assertTrue(Math.abs(expected.getColumnExpandRatio(col)
- - result.getColumnExpandRatio(col)) < 0.00001);
++ Assert.assertEquals(expected.getRowExpandRatio(row),
++ result.getRowExpandRatio(row), 0.00001);
+ }
+ for (int col = 0; col < expected.getColumns(); ++col) {
++ Assert.assertEquals(expected.getColumnExpandRatio(col),
++ result.getColumnExpandRatio(col), 0.00001);
+ }
++ for (int row = 0; row < expected.getRows(); ++row) {
++ for (int col = 0; col < expected.getColumns(); ++col) {
++ Component eC = expected.getComponent(col, row);
++ Component rC = result.getComponent(col, row);
++
++ assertEquals(eC, rC);
++ if (eC == null) {
++ continue;
++ }
++
++ Assert.assertEquals(expected.getComponentAlignment(eC),
++ result.getComponentAlignment(rC));
++
++ }
++ }
++
+ return result;
+ }
+
+ @Test
+ public void testNestedGridLayouts() {
+ String design = "<!DOCTYPE html>" + //
+ "<html>" + //
+ " <body> " + //
+ " <vaadin-grid-layout> " + //
+ " <row> " + //
+ " <column> " + //
+ " <vaadin-grid-layout> " + //
+ " <row> " + //
+ " <column> " + //
+ " <vaadin-button>" + //
+ " Button " + //
+ " </vaadin-button> " + //
+ " </column> " + //
+ " </row> " + //
+ " </vaadin-grid-layout> " + //
+ " </column> " + //
+ " </row> " + //
+ " </vaadin-grid-layout> " + //
+ " </body>" + //
+ "</html>";
+ GridLayout outer = new GridLayout();
+ GridLayout inner = new GridLayout();
+ Button b = new Button("Button");
+ b.setCaptionAsHtml(true);
+ inner.addComponent(b);
+ outer.addComponent(inner);
+ testRead(design, outer);
+ testWrite(design, outer);
+
+ }
+
+ @Test
+ public void testEmptyGridLayoutWithColsAndRowsSet() throws IOException {
+ GridLayout layout = new GridLayout();
+ layout.setRows(2);
+ layout.setColumns(2);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ DesignContext context = new DesignContext();
+ context.setRootComponent(layout);
+ Design.write(context, out);
+
+ ByteArrayInputStream input = new ByteArrayInputStream(out.toByteArray());
+ Component component = Design.read(input);
+ GridLayout readLayout = (GridLayout) component;
+
+ Assert.assertEquals(layout.getRows(), readLayout.getRows());
+ }
+
+ @Test
+ public void testGridLayoutAlignments() {
+ String design = "<vaadin-grid-layout><row>" //
+ + "<column><vaadin-label :middle>0</label></column>"//
+ + "<column><vaadin-label :right>1</label>"//
+ + "</row><row>" //
+ + "<column><vaadin-label :bottom :center>2</label></column>"//
+ + "<column><vaadin-label :middle :center>3</label>" //
+ + "</row></vaadin-grid-layout>";
+ GridLayout gl = new GridLayout(2, 2);
+
+ Alignment[] alignments = { Alignment.MIDDLE_LEFT, Alignment.TOP_RIGHT,
+ Alignment.BOTTOM_CENTER, Alignment.MIDDLE_CENTER };
+ for (int i = 0; i < 4; i++) {
+ Label child = new Label("" + i, ContentMode.HTML);
+ gl.addComponent(child);
+ gl.setComponentAlignment(child, alignments[i]);
+ }
+
+ testWrite(design, gl);
+ testRead(design, gl);
+ }
+}
--- /dev/null
- setMargins(top, right, bottom, left);
+/*
+ * Copyright 2000-2014 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.shared.ui;
+
+import java.io.Serializable;
+
+/**
+ * Describes the margin settings for each edge of a Component.
+ *
+ * @author Vaadin Ltd
+ */
+public class MarginInfo implements Serializable {
+
+ private static final int TOP = 1;
+ private static final int RIGHT = 2;
+ private static final int BOTTOM = 4;
+ private static final int LEFT = 8;
+ private static final int ALL = TOP | RIGHT | BOTTOM | LEFT;
+
+ private int bitMask;
+
+ /**
+ * Creates a MarginInfo object with all edges set to either enabled or
+ * disabled.
+ *
+ * @param enabled
+ * the value to set for all edges
+ */
+ public MarginInfo(boolean enabled) {
+ setMargins(enabled);
+ }
+
+ /**
+ * Creates a MarginInfo object from a bit mask.
+ *
+ * @param bitMask
+ * bits to set
+ */
+ public MarginInfo(int bitMask) {
+ this.bitMask = bitMask;
+ }
+
+ /**
+ * Creates a MarginInfo object by having each edge specified in clockwise
+ * order (analogous to CSS).
+ *
+ * @param top
+ * enable or disable top margin
+ * @param right
+ * enable or disable right margin
+ * @param bottom
+ * enable or disable bottom margin
+ * @param left
+ * enable or disable left margin
+ */
+ public MarginInfo(boolean top, boolean right, boolean bottom, boolean left) {
- bitMask = top ? TOP : 0;
- bitMask += right ? RIGHT : 0;
- bitMask += bottom ? BOTTOM : 0;
- bitMask += left ? LEFT : 0;
++ doSetMargins(top, right, bottom, left);
++ }
++
++ public MarginInfo(boolean vertical, boolean horizontal) {
++ this(vertical, horizontal, vertical, horizontal);
+ }
+
+ /**
+ * Enables or disables margins on all edges simultaneously.
+ *
+ * @param enabled
+ * if true, enables margins on all edges. If false, disables
+ * margins on all edges.
+ */
+ public void setMargins(boolean enabled) {
+ bitMask = enabled ? ALL : 0;
+ }
+
+ /**
+ * Sets margins on all edges individually.
+ *
+ * @param top
+ * enable or disable top margin
+ * @param right
+ * enable or disable right margin
+ * @param bottom
+ * enable or disable bottom margin
+ * @param left
+ * enable or disable left margin
+ */
+ public void setMargins(boolean top, boolean right, boolean bottom,
+ boolean left) {
++ doSetMargins(top, right, bottom, left);
+ }
+
+ /**
+ * Copies margin values from another MarginInfo object.
+ *
+ * @param marginInfo
+ * another marginInfo object
+ */
+ public void setMargins(MarginInfo marginInfo) {
+ bitMask = marginInfo.bitMask;
+ }
+
+ /**
+ * Checks if this MarginInfo object has margins on all edges enabled.
+ *
+ * @since 7.5.0
+ *
+ * @return true if all edges have margins enabled
+ */
+ public boolean hasAll() {
+ return (bitMask & ALL) == ALL;
+ }
+
+ /**
+ * Checks if this MarginInfo object has the left edge margin enabled.
+ *
+ * @return true if left edge margin is enabled
+ */
+ public boolean hasLeft() {
+ return (bitMask & LEFT) == LEFT;
+ }
+
+ /**
+ * Checks if this MarginInfo object has the right edge margin enabled.
+ *
+ * @return true if right edge margin is enabled
+ */
+ public boolean hasRight() {
+ return (bitMask & RIGHT) == RIGHT;
+ }
+
+ /**
+ * Checks if this MarginInfo object has the top edge margin enabled.
+ *
+ * @return true if top edge margin is enabled
+ */
+ public boolean hasTop() {
+ return (bitMask & TOP) == TOP;
+ }
+
+ /**
+ * Checks if this MarginInfo object has the bottom edge margin enabled.
+ *
+ * @return true if bottom edge margin is enabled
+ */
+ public boolean hasBottom() {
+ return (bitMask & BOTTOM) == BOTTOM;
+ }
+
+ /**
+ * Returns the current bit mask that make up the margin settings.
+ *
+ * @return an integer bit mask
+ */
+ public int getBitMask() {
+ return bitMask;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof MarginInfo)) {
+ return false;
+ }
+
+ return ((MarginInfo) obj).bitMask == bitMask;
+ }
+
+ @Override
+ public int hashCode() {
+ return bitMask;
+ }
+
+ @Override
+ public String toString() {
+ return "MarginInfo(" + hasTop() + ", " + hasRight() + ", "
+ + hasBottom() + ", " + hasLeft() + ")";
+
+ }
++
++ private void doSetMargins(boolean top, boolean right, boolean bottom,
++ boolean left) {
++ bitMask = top ? TOP : 0;
++ bitMask += right ? RIGHT : 0;
++ bitMask += bottom ? BOTTOM : 0;
++ bitMask += left ? LEFT : 0;
++ }
++
+}
--- /dev/null
--- /dev/null
++/*
++ * Copyright 2000-2014 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.shared.ui.draganddropwrapper;
++
++import com.vaadin.shared.communication.ServerRpc;
++
++/**
++ * RPC interface for calls from client to server.
++ *
++ * @since 7.6.4
++ * @author Vaadin Ltd
++ */
++public interface DragAndDropWrapperServerRpc extends ServerRpc {
++
++ /**
++ * Called to poll the server to see if any changes have been made e.g. when
++ * the upload is complete.
++ */
++ public void poll();
++
++}
</parallel>
</target>
- <target name="test-tb2" depends="clean-testbench-errors">
- <property name="war.file"
- location="${vaadin.basedir}/result/artifacts/${vaadin.version}/vaadin-uitest/vaadin-uitest-${vaadin.version}.war" />
- <parallel>
- <daemons>
- <ant antfile="${uitest.dir}/vaadin-server.xml"
- inheritall="true" inheritrefs="true" target="deploy-and-start" />
- </daemons>
- <sequential>
- <ant antfile="${uitest.dir}/vaadin-server.xml"
- target="wait-for-startup" />
- <ant antfile="${uitest.dir}/test.xml" target="tb2-tests" />
- </sequential>
- </parallel>
- </target>
-
<target name="test-tb3" depends="clean-testbench-errors">
<property name="war.file"
- location="${vaadin.basedir}/result/artifacts/${vaadin.version}/vaadin-uitest/vaadin-uitest-${vaadin.version}.war" />
+ location="${vaadin.basedir}/uitest/result/lib/vaadin-uitest-${vaadin.version}.war" />
+ <mkdir dir="${vaadin.basedir}/result" />
<parallel>
<daemons>
<ant antfile="${uitest.dir}/vaadin-server.xml"