summaryrefslogtreecommitdiffstats
path: root/compatibility-client
diff options
context:
space:
mode:
authorTeemu Suo-Anttila <teemusa@vaadin.com>2016-08-26 14:53:46 +0300
committerTeemu Suo-Anttila <teemusa@vaadin.com>2016-08-30 13:04:57 +0300
commitd40910015746be496ba1ded4c12a019d801adb5e (patch)
tree608d951758d25098cb6d6e9e5f71f197e9bd5f77 /compatibility-client
parent51b27217e21e99b059c178afb497ffd7a52e91af (diff)
downloadvaadin-framework-d40910015746be496ba1ded4c12a019d801adb5e.tar.gz
vaadin-framework-d40910015746be496ba1ded4c12a019d801adb5e.zip
Duplicate client-side of the Vaadin 7 Grid
Change-Id: I069df183806937c2d97eb3e9c8a073ef53ab5c24
Diffstat (limited to 'compatibility-client')
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractGridRendererConnector.java5
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractRendererConnector.java128
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractSelectionModelConnector.java4
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ButtonRendererConnector.java4
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ClickableRendererConnector.java6
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/GridConnector.java56
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ImageRendererConnector.java4
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/JavaScriptRendererConnector.java8
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java24
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/NoSelectionModelConnector.java4
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ProgressBarRendererConnector.java2
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/SingleSelectionModelConnector.java10
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/TextRendererConnector.java2
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/connectors/UnsafeHtmlRendererConnector.java4
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ButtonRenderer.java44
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ClickableRenderer.java234
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ComplexRenderer.java157
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/DateRenderer.java108
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/HtmlRenderer.java41
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ImageRenderer.java49
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/NumberRenderer.java71
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ProgressBarRenderer.java47
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/Renderer.java54
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/TextRenderer.java32
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/renderers/WidgetRenderer.java112
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/ui/JsniMousewheelHandler.java2
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Cell.java85
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/ColumnConfiguration.java198
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/EscalatorUpdater.java157
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/FlyweightCell.java201
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/FlyweightRow.java298
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/PositionFunction.java118
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Row.java49
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowContainer.java282
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeEvent.java99
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeHandler.java38
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/ScrollbarBundle.java869
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Spacer.java47
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/SpacerUpdater.java64
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/events/RowHeightChangedEvent.java48
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/events/RowHeightChangedHandler.java36
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/AutoScroller.java647
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/CellReference.java151
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/CellStyleGenerator.java40
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableEvent.java55
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableHandler.java37
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DefaultEditorEventHandler.java332
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DetailsGenerator.java46
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/EditorHandler.java174
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/EventCellReference.java128
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/HeightAwareDetailsGenerator.java45
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RendererCellReference.java93
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RowReference.java104
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RowStyleGenerator.java40
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/datasources/ListDataSource.java465
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/datasources/ListSorter.java176
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/AbstractGridKeyEventHandler.java44
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/AbstractGridMouseEventHandler.java39
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyClickHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyDoubleClickHandler.java29
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyDownHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyPressHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyUpHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnReorderEvent.java51
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnReorderHandler.java40
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnResizeEvent.java67
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnResizeHandler.java40
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnVisibilityChangeEvent.java93
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnVisibilityChangeHandler.java41
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterClickHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterDoubleClickHandler.java29
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyDownHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyPressHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyUpHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridClickEvent.java52
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridDoubleClickEvent.java55
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridEnabledEvent.java46
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridEnabledHandler.java37
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyDownEvent.java123
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyPressEvent.java76
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyUpEvent.java123
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderClickHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderDoubleClickHandler.java29
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyDownHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyPressHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyUpHandler.java28
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ScrollEvent.java40
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ScrollHandler.java35
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/SelectAllEvent.java59
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/SelectAllHandler.java37
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/AbstractRowHandleSelectionModel.java66
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/ClickSelectHandler.java77
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/HasSelectionHandlers.java42
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/MultiSelectionRenderer.java781
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionEvent.java178
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionHandler.java39
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModel.java258
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelMulti.java273
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelNone.java73
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelSingle.java175
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SpaceSelectHandler.java137
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/Sort.java154
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortEvent.java114
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortHandler.java38
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortOrder.java90
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Escalator.java6785
-rw-r--r--compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Grid.java8968
107 files changed, 26035 insertions, 68 deletions
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractGridRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractGridRendererConnector.java
index 0a7c8b68ef..dca307b9e7 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractGridRendererConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractGridRendererConnector.java
@@ -16,9 +16,8 @@
package com.vaadin.v7.client.connectors;
import com.vaadin.client.ServerConnector;
-import com.vaadin.client.connectors.AbstractRendererConnector;
-import com.vaadin.client.renderers.Renderer;
-import com.vaadin.client.widgets.Grid.Column;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widgets.Grid.Column;
import elemental.json.JsonObject;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractRendererConnector.java
new file mode 100644
index 0000000000..284cc225e0
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractRendererConnector.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2000-2016 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.v7.client.connectors;
+
+import com.vaadin.client.ServerConnector;
+import com.vaadin.client.communication.JsonDecoder;
+import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.client.metadata.NoDataException;
+import com.vaadin.client.metadata.Type;
+import com.vaadin.client.metadata.TypeData;
+import com.vaadin.client.metadata.TypeDataStore;
+import com.vaadin.v7.client.renderers.Renderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * An abstract base class for renderer connectors.
+ *
+ * @param <T>
+ * the presentation type of the renderer
+ */
+public abstract class AbstractRendererConnector<T>
+ extends AbstractExtensionConnector {
+
+ private Renderer<T> renderer = null;
+
+ private final Type presentationType = TypeDataStore
+ .getPresentationType(this.getClass());
+
+ protected AbstractRendererConnector() {
+ if (presentationType == null) {
+ throw new IllegalStateException("No presentation type found for "
+ + getClass().getSimpleName()
+ + ". This may be caused by some unspecified problem in widgetset compilation.");
+ }
+ }
+
+ /**
+ * Returns the renderer associated with this renderer connector.
+ * <p>
+ * A subclass of AbstractRendererConnector should override this method as
+ * shown below. The framework uses
+ * {@link com.google.gwt.core.client.GWT#create(Class) GWT.create(Class)} to
+ * create a renderer based on the return type of the overridden method, but
+ * only if {@link #createRenderer()} is not overridden as well:
+ *
+ * <pre>
+ * public MyRenderer getRenderer() {
+ * return (MyRenderer) super.getRenderer();
+ * }
+ * </pre>
+ *
+ * @return the renderer bound to this connector
+ */
+ public Renderer<T> getRenderer() {
+ if (renderer == null) {
+ renderer = createRenderer();
+ }
+ return renderer;
+ }
+
+ /**
+ * Creates a new Renderer instance associated with this renderer connector.
+ * <p>
+ * You should typically not override this method since the framework by
+ * default generates an implementation that uses
+ * {@link com.google.gwt.core.client.GWT#create(Class)} to create a renderer
+ * of the same type as returned by the most specific override of
+ * {@link #getRenderer()}. If you do override the method, you can't call
+ * <code>super.createRenderer()</code> since the metadata needed for that
+ * implementation is not generated if there's an override of the method.
+ *
+ * @return a new renderer to be used with this connector
+ */
+ protected Renderer<T> createRenderer() {
+ // TODO generate type data
+ Type type = TypeData.getType(getClass());
+ try {
+ Type rendererType = type.getMethod("getRenderer").getReturnType();
+ @SuppressWarnings("unchecked")
+ Renderer<T> instance = (Renderer<T>) rendererType.createInstance();
+ return instance;
+ } catch (NoDataException e) {
+ throw new IllegalStateException(
+ "Default implementation of createRenderer() does not work for "
+ + getClass().getSimpleName()
+ + ". This might be caused by explicitely using "
+ + "super.createRenderer() or some unspecified "
+ + "problem with the widgetset compilation.",
+ e);
+ }
+ }
+
+ /**
+ * Decodes the given JSON value into a value of type T so it can be passed
+ * to the {@link #getRenderer() renderer}.
+ *
+ * @param value
+ * the value to decode
+ * @return the decoded value of {@code value}
+ */
+ public T decode(JsonValue value) {
+ @SuppressWarnings("unchecked")
+ T decodedValue = (T) JsonDecoder.decodeValue(presentationType, value,
+ null, getConnection());
+ return decodedValue;
+ }
+
+ @Override
+ @Deprecated
+ protected void extend(ServerConnector target) {
+ // NOOP
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractSelectionModelConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractSelectionModelConnector.java
index c1a0cf9123..577f7d67b2 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractSelectionModelConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/AbstractSelectionModelConnector.java
@@ -19,9 +19,9 @@ import java.util.Collection;
import com.vaadin.client.data.DataSource.RowHandle;
import com.vaadin.client.extensions.AbstractExtensionConnector;
-import com.vaadin.client.widget.grid.selection.SelectionModel;
-import com.vaadin.client.widgets.Grid;
import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel;
+import com.vaadin.v7.client.widgets.Grid;
import elemental.json.JsonObject;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ButtonRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ButtonRendererConnector.java
index df59418326..6e79089ca9 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ButtonRendererConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ButtonRendererConnector.java
@@ -16,9 +16,9 @@
package com.vaadin.v7.client.connectors;
import com.google.web.bindery.event.shared.HandlerRegistration;
-import com.vaadin.client.renderers.ButtonRenderer;
-import com.vaadin.client.renderers.ClickableRenderer.RendererClickHandler;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.v7.client.renderers.ButtonRenderer;
+import com.vaadin.v7.client.renderers.ClickableRenderer.RendererClickHandler;
import elemental.json.JsonObject;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ClickableRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ClickableRendererConnector.java
index 3049fa9883..6a4bf0bb05 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ClickableRendererConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ClickableRendererConnector.java
@@ -17,10 +17,10 @@ package com.vaadin.v7.client.connectors;
import com.google.web.bindery.event.shared.HandlerRegistration;
import com.vaadin.client.MouseEventDetailsBuilder;
-import com.vaadin.client.renderers.ClickableRenderer;
-import com.vaadin.client.renderers.ClickableRenderer.RendererClickEvent;
-import com.vaadin.client.renderers.ClickableRenderer.RendererClickHandler;
import com.vaadin.shared.ui.grid.renderers.RendererClickRpc;
+import com.vaadin.v7.client.renderers.ClickableRenderer;
+import com.vaadin.v7.client.renderers.ClickableRenderer.RendererClickEvent;
+import com.vaadin.v7.client.renderers.ClickableRenderer.RendererClickHandler;
import elemental.json.JsonObject;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/GridConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/GridConnector.java
index 1bf3d7a8ea..fb634f0d90 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/GridConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/GridConnector.java
@@ -50,34 +50,6 @@ import com.vaadin.client.ui.AbstractComponentConnector;
import com.vaadin.client.ui.AbstractHasComponentsConnector;
import com.vaadin.client.ui.ConnectorFocusAndBlurHandler;
import com.vaadin.client.ui.SimpleManagedLayout;
-import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent;
-import com.vaadin.client.widget.escalator.events.RowHeightChangedHandler;
-import com.vaadin.client.widget.grid.CellReference;
-import com.vaadin.client.widget.grid.CellStyleGenerator;
-import com.vaadin.client.widget.grid.EditorHandler;
-import com.vaadin.client.widget.grid.EventCellReference;
-import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator;
-import com.vaadin.client.widget.grid.RowReference;
-import com.vaadin.client.widget.grid.RowStyleGenerator;
-import com.vaadin.client.widget.grid.events.BodyClickHandler;
-import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler;
-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.GridClickEvent;
-import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
-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.Grid;
-import com.vaadin.client.widgets.Grid.Column;
-import com.vaadin.client.widgets.Grid.FooterCell;
-import com.vaadin.client.widgets.Grid.FooterRow;
-import com.vaadin.client.widgets.Grid.HeaderCell;
-import com.vaadin.client.widgets.Grid.HeaderRow;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.Connect;
@@ -95,6 +67,34 @@ import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.v7.client.connectors.RpcDataSourceConnector.DetailsListener;
import com.vaadin.v7.client.connectors.RpcDataSourceConnector.RpcDataSource;
+import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedEvent;
+import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedHandler;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.CellStyleGenerator;
+import com.vaadin.v7.client.widget.grid.EditorHandler;
+import com.vaadin.v7.client.widget.grid.EventCellReference;
+import com.vaadin.v7.client.widget.grid.HeightAwareDetailsGenerator;
+import com.vaadin.v7.client.widget.grid.RowReference;
+import com.vaadin.v7.client.widget.grid.RowStyleGenerator;
+import com.vaadin.v7.client.widget.grid.events.BodyClickHandler;
+import com.vaadin.v7.client.widget.grid.events.BodyDoubleClickHandler;
+import com.vaadin.v7.client.widget.grid.events.ColumnReorderEvent;
+import com.vaadin.v7.client.widget.grid.events.ColumnReorderHandler;
+import com.vaadin.v7.client.widget.grid.events.ColumnResizeEvent;
+import com.vaadin.v7.client.widget.grid.events.ColumnResizeHandler;
+import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeEvent;
+import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeHandler;
+import com.vaadin.v7.client.widget.grid.events.GridClickEvent;
+import com.vaadin.v7.client.widget.grid.events.GridDoubleClickEvent;
+import com.vaadin.v7.client.widget.grid.sort.SortEvent;
+import com.vaadin.v7.client.widget.grid.sort.SortHandler;
+import com.vaadin.v7.client.widget.grid.sort.SortOrder;
+import com.vaadin.v7.client.widgets.Grid;
+import com.vaadin.v7.client.widgets.Grid.Column;
+import com.vaadin.v7.client.widgets.Grid.FooterCell;
+import com.vaadin.v7.client.widgets.Grid.FooterRow;
+import com.vaadin.v7.client.widgets.Grid.HeaderCell;
+import com.vaadin.v7.client.widgets.Grid.HeaderRow;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ImageRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ImageRendererConnector.java
index 092e01e626..0f3dfc1cc5 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ImageRendererConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ImageRendererConnector.java
@@ -18,10 +18,10 @@ package com.vaadin.v7.client.connectors;
import com.google.web.bindery.event.shared.HandlerRegistration;
import com.vaadin.client.communication.JsonDecoder;
import com.vaadin.client.metadata.TypeDataStore;
-import com.vaadin.client.renderers.ClickableRenderer.RendererClickHandler;
-import com.vaadin.client.renderers.ImageRenderer;
import com.vaadin.shared.communication.URLReference;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.v7.client.renderers.ImageRenderer;
+import com.vaadin.v7.client.renderers.ClickableRenderer.RendererClickHandler;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/JavaScriptRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/JavaScriptRendererConnector.java
index 7213b6a846..df12c5aacb 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/JavaScriptRendererConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/JavaScriptRendererConnector.java
@@ -24,12 +24,12 @@ import com.google.gwt.dom.client.NativeEvent;
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.v7.client.renderers.ComplexRenderer;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
import com.vaadin.v7.ui.renderers.AbstractJavaScriptRenderer;
import elemental.json.JsonObject;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java
index ac52a02056..ffb100bd71 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java
@@ -29,23 +29,23 @@ import com.vaadin.client.ServerConnector;
import com.vaadin.client.annotations.OnStateChange;
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.widget.grid.DataAvailableEvent;
-import com.vaadin.client.widget.grid.DataAvailableHandler;
-import com.vaadin.client.widget.grid.events.SelectAllEvent;
-import com.vaadin.client.widget.grid.events.SelectAllHandler;
-import com.vaadin.client.widget.grid.selection.MultiSelectionRenderer;
-import com.vaadin.client.widget.grid.selection.SelectionModel;
-import com.vaadin.client.widget.grid.selection.SelectionModel.Multi;
-import com.vaadin.client.widget.grid.selection.SpaceSelectHandler;
-import com.vaadin.client.widgets.Grid;
-import com.vaadin.client.widgets.Grid.HeaderCell;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.Range;
import com.vaadin.shared.ui.grid.selection.MultiSelectionModelServerRpc;
import com.vaadin.shared.ui.grid.selection.MultiSelectionModelState;
+import com.vaadin.v7.client.renderers.ComplexRenderer;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widget.grid.DataAvailableEvent;
+import com.vaadin.v7.client.widget.grid.DataAvailableHandler;
+import com.vaadin.v7.client.widget.grid.events.SelectAllEvent;
+import com.vaadin.v7.client.widget.grid.events.SelectAllHandler;
+import com.vaadin.v7.client.widget.grid.selection.MultiSelectionRenderer;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel;
+import com.vaadin.v7.client.widget.grid.selection.SpaceSelectHandler;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Multi;
+import com.vaadin.v7.client.widgets.Grid;
+import com.vaadin.v7.client.widgets.Grid.HeaderCell;
import com.vaadin.v7.ui.Grid.MultiSelectionModel;
import elemental.json.JsonObject;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/NoSelectionModelConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/NoSelectionModelConnector.java
index bbbd462ab5..8733a457ff 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/NoSelectionModelConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/NoSelectionModelConnector.java
@@ -16,9 +16,9 @@
package com.vaadin.v7.client.connectors;
import com.vaadin.client.ServerConnector;
-import com.vaadin.client.widget.grid.selection.SelectionModel;
-import com.vaadin.client.widget.grid.selection.SelectionModelNone;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModelNone;
import com.vaadin.v7.ui.Grid.NoSelectionModel;
import elemental.json.JsonObject;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ProgressBarRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ProgressBarRendererConnector.java
index 0fda3114ec..9f32a1f307 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ProgressBarRendererConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/ProgressBarRendererConnector.java
@@ -15,8 +15,8 @@
*/
package com.vaadin.v7.client.connectors;
-import com.vaadin.client.renderers.ProgressBarRenderer;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.v7.client.renderers.ProgressBarRenderer;
/**
* A connector for {@link ProgressBarRenderer}.
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/SingleSelectionModelConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/SingleSelectionModelConnector.java
index 0e6702027e..9d5233607d 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/SingleSelectionModelConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/SingleSelectionModelConnector.java
@@ -18,15 +18,15 @@ package com.vaadin.v7.client.connectors;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.data.DataSource.RowHandle;
-import com.vaadin.client.renderers.Renderer;
-import com.vaadin.client.widget.grid.selection.ClickSelectHandler;
-import com.vaadin.client.widget.grid.selection.SelectionModel;
-import com.vaadin.client.widget.grid.selection.SelectionModel.Single;
-import com.vaadin.client.widget.grid.selection.SpaceSelectHandler;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.selection.SingleSelectionModelServerRpc;
import com.vaadin.shared.ui.grid.selection.SingleSelectionModelState;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widget.grid.selection.ClickSelectHandler;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel;
+import com.vaadin.v7.client.widget.grid.selection.SpaceSelectHandler;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Single;
import com.vaadin.v7.ui.Grid.SingleSelectionModel;
import elemental.json.JsonObject;
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/TextRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/TextRendererConnector.java
index 03775ec6f0..df79ca2497 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/TextRendererConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/TextRendererConnector.java
@@ -15,8 +15,8 @@
*/
package com.vaadin.v7.client.connectors;
-import com.vaadin.client.renderers.TextRenderer;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.v7.client.renderers.TextRenderer;
/**
* A connector for {@link TextRenderer}.
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/UnsafeHtmlRendererConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/UnsafeHtmlRendererConnector.java
index 36f58e302b..e9828fc01e 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/UnsafeHtmlRendererConnector.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/UnsafeHtmlRendererConnector.java
@@ -15,9 +15,9 @@
*/
package com.vaadin.v7.client.connectors;
-import com.vaadin.client.renderers.Renderer;
-import com.vaadin.client.widget.grid.RendererCellReference;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
/**
* A connector for {@link UnsafeHtmlRenderer}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ButtonRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ButtonRenderer.java
new file mode 100644
index 0000000000..250c91a4fb
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ButtonRenderer.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.google.gwt.core.shared.GWT;
+import com.google.gwt.user.client.ui.Button;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * A Renderer that displays buttons with textual captions. The values of the
+ * corresponding column are used as the captions. Click handlers can be added to
+ * the renderer, invoked when any of the rendered buttons is clicked.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ButtonRenderer extends ClickableRenderer<String, Button> {
+
+ @Override
+ public Button createWidget() {
+ Button b = GWT.create(Button.class);
+ b.addClickHandler(this);
+ b.setStylePrimaryName("v-nativebutton");
+ return b;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, String text, Button button) {
+ button.setText(text);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ClickableRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ClickableRenderer.java
new file mode 100644
index 0000000000..e99d03d3d1
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ClickableRenderer.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.dom.client.MouseEvent;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.HandlerManager;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.v7.client.widget.escalator.Cell;
+import com.vaadin.v7.client.widget.escalator.RowContainer;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.EventCellReference;
+import com.vaadin.v7.client.widgets.Escalator;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * An abstract superclass for renderers that render clickable widgets. Click
+ * handlers can be added to a renderer to listen to click events emitted by all
+ * widgets rendered by the renderer.
+ *
+ * @param <T>
+ * the presentation (column) type
+ * @param <W>
+ * the widget type
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract class ClickableRenderer<T, W extends Widget>
+ extends WidgetRenderer<T, W> implements ClickHandler {
+
+ /**
+ * A handler for {@link RendererClickEvent renderer click events}.
+ *
+ * @param <R>
+ * the row type of the containing Grid
+ *
+ * @see {@link ButtonRenderer#addClickHandler(RendererClickHandler)}
+ */
+ public interface RendererClickHandler<R> extends EventHandler {
+
+ /**
+ * Called when a rendered button is clicked.
+ *
+ * @param event
+ * the event representing the click
+ */
+ void onClick(RendererClickEvent<R> event);
+ }
+
+ /**
+ * An event fired when a widget rendered by a ClickableWidgetRenderer
+ * subclass is clicked.
+ *
+ * @param <R>
+ * the row type of the containing Grid
+ */
+ @SuppressWarnings("rawtypes")
+ public static class RendererClickEvent<R>
+ extends MouseEvent<RendererClickHandler> {
+
+ @SuppressWarnings("unchecked")
+ static final Type<RendererClickHandler> TYPE = new Type<RendererClickHandler>(
+ BrowserEvents.CLICK, new RendererClickEvent());
+
+ private CellReference<R> cell;
+
+ private R row;
+
+ private RendererClickEvent() {
+ }
+
+ /**
+ * Returns the cell of the clicked button.
+ *
+ * @return the cell
+ */
+ public CellReference<R> getCell() {
+ return cell;
+ }
+
+ /**
+ * Returns the data object corresponding to the row of the clicked
+ * button.
+ *
+ * @return the row data object
+ */
+ public R getRow() {
+ return row;
+ }
+
+ @Override
+ public Type<RendererClickHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void dispatch(RendererClickHandler handler) {
+
+ EventTarget target = getNativeEvent().getEventTarget();
+
+ if (!Element.is(target)) {
+ return;
+ }
+
+ Element e = Element.as(target);
+ Grid<R> grid = (Grid<R>) findClosestParentGrid(e);
+
+ cell = findCell(grid, e);
+ row = cell.getRow();
+
+ handler.onClick(this);
+ }
+
+ /**
+ * Returns the cell the given element belongs to.
+ *
+ * @param grid
+ * the grid instance that is queried
+ * @param e
+ * a cell element or the descendant of one
+ * @return the cell or null if the element is not a grid cell or a
+ * descendant of one
+ */
+ private static <T> CellReference<T> findCell(Grid<T> grid, Element e) {
+ RowContainer container = getEscalator(grid).findRowContainer(e);
+ if (container == null) {
+ return null;
+ }
+ Cell cell = container.getCell(e);
+ EventCellReference<T> cellReference = new EventCellReference<T>(
+ grid);
+ // FIXME: Section is currently always body. Might be useful for the
+ // future to have an actual check.
+ cellReference.set(cell, Section.BODY);
+ return cellReference;
+ }
+
+ private native static Escalator getEscalator(Grid<?> grid)
+ /*-{
+ return grid.@com.vaadin.v7.client.widgets.Grid::escalator;
+ }-*/;
+
+ /**
+ * Returns the Grid instance containing the given element, if any.
+ * <p>
+ * <strong>Note:</strong> This method may not work reliably if the grid
+ * in question is wrapped in a {@link Composite} <em>unless</em> the
+ * element is inside another widget that is a child of the wrapped grid;
+ * please refer to the note in
+ * {@link WidgetUtil#findWidget(Element, Class) Util.findWidget} for
+ * details.
+ *
+ * @param e
+ * the element whose parent grid to find
+ * @return the parent grid or null if none found.
+ */
+ private static Grid<?> findClosestParentGrid(Element e) {
+ Widget w = WidgetUtil.findWidget(e, null);
+
+ while (w != null && !(w instanceof Grid)) {
+ w = w.getParent();
+ }
+ return (Grid<?>) w;
+ }
+ }
+
+ private HandlerManager handlerManager;
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation note:</em> It is the implementing method's
+ * responsibility to add {@code this} as a click handler of the returned
+ * widget, or a widget nested therein, in order to make click events
+ * propagate properly to handlers registered via
+ * {@link #addClickHandler(RendererClickHandler) addClickHandler}.
+ */
+ @Override
+ public abstract W createWidget();
+
+ /**
+ * Adds a click handler to this button renderer. The handler is invoked
+ * every time one of the widgets rendered by this renderer is clicked.
+ * <p>
+ * Note that the row type of the click handler must match the row type of
+ * the containing Grid.
+ *
+ * @param handler
+ * the click handler to be added
+ */
+ public HandlerRegistration addClickHandler(
+ RendererClickHandler<?> handler) {
+ if (handlerManager == null) {
+ handlerManager = new HandlerManager(this);
+ }
+ return handlerManager.addHandler(RendererClickEvent.TYPE, handler);
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ /*
+ * The handler manager is lazily instantiated so it's null iff
+ * addClickHandler is never called.
+ */
+ if (handlerManager != null) {
+ DomEvent.fireNativeEvent(event.getNativeEvent(), handlerManager);
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ComplexRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ComplexRenderer.java
new file mode 100644
index 0000000000..330d1874af
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ComplexRenderer.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import java.util.Collection;
+import java.util.Collections;
+
+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.Style.Visibility;
+import com.vaadin.v7.client.widget.escalator.Cell;
+import com.vaadin.v7.client.widget.escalator.FlyweightCell;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * Base class for renderers that needs initialization and destruction logic
+ * (override {@link #init(FlyweightCell)} and {@link #destroy(FlyweightCell) }
+ * and event handling (see {@link #onBrowserEvent(Cell, NativeEvent)},
+ * {@link #getConsumedEvents()} and {@link #onActivate()}.
+ *
+ * <p>
+ * Also provides a helper method for hiding the cell contents by overriding
+ * {@link #setContentVisible(FlyweightCell, boolean)}
+ *
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public abstract class ComplexRenderer<T> implements Renderer<T> {
+
+ /**
+ * Called at initialization stage. Perform any initialization here e.g.
+ * attach handlers, attach widgets etc.
+ *
+ * @param cell
+ * The cell. Note that the cell is not to be stored outside of
+ * the method as the cell instance will change. See
+ * {@link FlyweightCell}
+ */
+ public abstract void init(RendererCellReference cell);
+
+ /**
+ * Called after the cell is deemed to be destroyed and no longer used by the
+ * Grid. Called after the cell element is detached from the DOM.
+ * <p>
+ * The row object in the cell reference will be <code>null</code> since the
+ * row might no longer be present in the data source.
+ *
+ * @param cell
+ * The cell. Note that the cell is not to be stored outside of
+ * the method as the cell instance will change. See
+ * {@link FlyweightCell}
+ */
+ public void destroy(RendererCellReference cell) {
+ // Implement if needed
+ }
+
+ /**
+ * Returns the events that the renderer should consume. These are also the
+ * events that the Grid will pass to
+ * {@link #onBrowserEvent(Cell, NativeEvent)} when they occur.
+ *
+ * @return a list of consumed events
+ *
+ * @see com.google.gwt.dom.client.BrowserEvents
+ */
+ public Collection<String> getConsumedEvents() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Called whenever a registered event is triggered in the column the
+ * renderer renders.
+ * <p>
+ * The events that triggers this needs to be returned by the
+ * {@link #getConsumedEvents()} method.
+ * <p>
+ * Returns boolean telling if the event has been completely handled and
+ * should not cause any other actions.
+ *
+ * @param cell
+ * Object containing information about the cell the event was
+ * triggered on.
+ *
+ * @param event
+ * The original DOM event
+ * @return true if event should not be handled by grid
+ */
+ public boolean onBrowserEvent(CellReference<?> cell, NativeEvent event) {
+ return false;
+ }
+
+ /**
+ * Used by Grid to toggle whether to show actual data or just an empty
+ * placeholder while data is loading. This method is invoked whenever a cell
+ * changes between data being available and data missing.
+ * <p>
+ * Default implementation hides content by setting visibility: hidden to all
+ * elements inside the cell. Text nodes are left as is - renderers that add
+ * such to the root element need to implement explicit support hiding them.
+ *
+ * @param cell
+ * The cell
+ * @param hasData
+ * Has the cell content been loaded from the data source
+ *
+ */
+ public void setContentVisible(RendererCellReference cell, boolean hasData) {
+ Element cellElement = cell.getElement();
+ for (int n = 0; n < cellElement.getChildCount(); n++) {
+ Node node = cellElement.getChild(n);
+ if (Element.is(node)) {
+ Element e = Element.as(node);
+ if (hasData) {
+ e.getStyle().clearVisibility();
+ } else {
+ e.getStyle().setVisibility(Visibility.HIDDEN);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when the cell is activated by pressing <code>enter</code>, double
+ * clicking or performing a double tap on the cell.
+ *
+ * @param cell
+ * the activated cell
+ * @return <code>true</code> if event was handled and should not be
+ * interpreted as a generic gesture by Grid.
+ */
+ public boolean onActivate(CellReference<?> cell) {
+ return false;
+ }
+
+ /**
+ * Called when the renderer is deemed to be destroyed and no longer used by
+ * the Grid.
+ */
+ public void destroy() {
+ // Implement if needed
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/DateRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/DateRenderer.java
new file mode 100644
index 0000000000..85d489d389
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/DateRenderer.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.TimeZone;
+import com.google.gwt.i18n.shared.DateTimeFormat;
+import com.google.gwt.i18n.shared.DateTimeFormat.PredefinedFormat;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * A renderer for rendering dates into cells
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DateRenderer implements Renderer<Date> {
+
+ private DateTimeFormat format;
+
+ // Calendar is unavailable for GWT
+ @SuppressWarnings("deprecation")
+ private TimeZone timeZone = TimeZone
+ .createTimeZone(new Date().getTimezoneOffset());
+
+ public DateRenderer() {
+ this(PredefinedFormat.DATE_TIME_SHORT);
+ }
+
+ public DateRenderer(PredefinedFormat format) {
+ this(DateTimeFormat.getFormat(format));
+ }
+
+ public DateRenderer(DateTimeFormat format) {
+ setFormat(format);
+ }
+
+ @Override
+ public void render(RendererCellReference cell, Date date) {
+ String dateStr = format.format(date, timeZone);
+ cell.getElement().setInnerText(dateStr);
+ }
+
+ /**
+ * Gets the format of how the date is formatted.
+ *
+ * @return the format
+ * @see <a href=
+ * "http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/shared/DateTimeFormat.html">GWT
+ * documentation on DateTimeFormat</a>
+ */
+ public DateTimeFormat getFormat() {
+ return format;
+ }
+
+ /**
+ * Sets the format used for formatting the dates.
+ *
+ * @param format
+ * the format to set
+ * @see <a href=
+ * "http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/shared/DateTimeFormat.html">GWT
+ * documentation on DateTimeFormat</a>
+ */
+ public void setFormat(DateTimeFormat format) {
+ if (format == null) {
+ throw new IllegalArgumentException("Format should not be null");
+ }
+ this.format = format;
+ }
+
+ /**
+ * Returns the time zone of the date.
+ *
+ * @return the time zone
+ */
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ /**
+ * Sets the time zone of the the date. By default uses the time zone of the
+ * browser.
+ *
+ * @param timeZone
+ * the timeZone to set
+ */
+ public void setTimeZone(TimeZone timeZone) {
+ if (timeZone == null) {
+ throw new IllegalArgumentException("Timezone should not be null");
+ }
+ this.timeZone = timeZone;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/HtmlRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/HtmlRenderer.java
new file mode 100644
index 0000000000..768dd15215
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/HtmlRenderer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * Renders a string as HTML into a cell.
+ * <p>
+ * The html string is rendered as is without any escaping. It is up to the
+ * developer to ensure that the html string honors the {@link SafeHtml}
+ * contract. For more information see
+ * {@link SafeHtmlUtils#fromSafeConstant(String)}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see SafeHtmlUtils#fromSafeConstant(String)
+ */
+public class HtmlRenderer implements Renderer<String> {
+
+ @Override
+ public void render(RendererCellReference cell, String htmlString) {
+ cell.getElement()
+ .setInnerSafeHtml(SafeHtmlUtils.fromSafeConstant(htmlString));
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ImageRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ImageRenderer.java
new file mode 100644
index 0000000000..a29b4f2cfd
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ImageRenderer.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.Image;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * A renderer that renders an image into a cell. Click handlers can be added to
+ * the renderer, invoked every time any of the images rendered by that rendered
+ * is clicked.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ImageRenderer extends ClickableRenderer<String, Image> {
+
+ public static final String TRANSPARENT_GIF_1PX = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
+
+ @Override
+ public Image createWidget() {
+ Image image = GWT.create(Image.class);
+ image.addClickHandler(this);
+ return image;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, String url, Image image) {
+ if (url == null) {
+ image.setUrl(TRANSPARENT_GIF_1PX);
+ } else {
+ image.setUrl(url);
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/NumberRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/NumberRenderer.java
new file mode 100644
index 0000000000..fb7762d39c
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/NumberRenderer.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.google.gwt.i18n.client.NumberFormat;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * Renders a number into a cell using a specific {@link NumberFormat}. By
+ * default uses the default number format returned by
+ * {@link NumberFormat#getDecimalFormat()}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @param <T>
+ * The number type to render.
+ */
+public class NumberRenderer implements Renderer<Number> {
+
+ private NumberFormat format;
+
+ public NumberRenderer() {
+ this(NumberFormat.getDecimalFormat());
+ }
+
+ public NumberRenderer(NumberFormat format) {
+ setFormat(format);
+ }
+
+ /**
+ * Gets the number format that the number should be formatted in.
+ *
+ * @return the number format used to render the number
+ */
+ public NumberFormat getFormat() {
+ return format;
+ }
+
+ /**
+ * Sets the number format to use for formatting the number.
+ *
+ * @param format
+ * the format to use
+ * @throws IllegalArgumentException
+ * when the format is null
+ */
+ public void setFormat(NumberFormat format) throws IllegalArgumentException {
+ if (format == null) {
+ throw new IllegalArgumentException("Format cannot be null");
+ }
+ this.format = format;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, Number number) {
+ cell.getElement().setInnerText(format.format(number));
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ProgressBarRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ProgressBarRenderer.java
new file mode 100644
index 0000000000..be4a3d0dfb
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/ProgressBarRenderer.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.google.gwt.core.shared.GWT;
+import com.vaadin.client.ui.VProgressBar;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * A Renderer that represents a double value as a graphical progress bar.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ProgressBarRenderer extends WidgetRenderer<Double, VProgressBar> {
+
+ @Override
+ public VProgressBar createWidget() {
+ VProgressBar progressBar = GWT.create(VProgressBar.class);
+ progressBar.addStyleDependentName("static");
+ return progressBar;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, Double data,
+ VProgressBar progressBar) {
+ if (data == null) {
+ progressBar.setEnabled(false);
+ } else {
+ progressBar.setEnabled(true);
+ progressBar.setState(data.floatValue());
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/Renderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/Renderer.java
new file mode 100644
index 0000000000..c113525aa0
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/Renderer.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.vaadin.v7.client.widget.escalator.Cell;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Renderer for rending a value &lt;T&gt; into cell.
+ * <p>
+ * You can add a renderer to any column by overring the
+ * {@link GridColumn#getRenderer()} method and returning your own renderer. You
+ * can retrieve the cell element using {@link Cell#getElement()}.
+ *
+ * @param <T>
+ * The column type
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> {
+
+ /**
+ * Called whenever the {@link Grid} updates a cell.
+ * <p>
+ * For optimal performance, work done in this method should be kept to a
+ * minimum since it will be called continuously while the user is scrolling.
+ * It is recommended to set up the cell's DOM structure in
+ * {@link ComplexRenderer#init(RendererCellReference)} and only make
+ * incremental updates based on cell data in this method.
+ *
+ * @param cell
+ * The cell. Note that the cell is a flyweight and should not be
+ * stored outside of the method as it will change.
+ *
+ * @param data
+ * The column data object
+ */
+ void render(RendererCellReference cell, T data);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/TextRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/TextRenderer.java
new file mode 100644
index 0000000000..a4dc7bdd00
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/TextRenderer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * Renderer that renders text into a cell.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class TextRenderer implements Renderer<String> {
+
+ @Override
+ public void render(RendererCellReference cell, String text) {
+ cell.getElement().setInnerText(text);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/WidgetRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/WidgetRenderer.java
new file mode 100644
index 0000000000..560c857a10
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/renderers/WidgetRenderer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2000-2016 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.v7.client.renderers;
+
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+
+/**
+ * A renderer for rendering widgets into cells.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row data type
+ * @param <W>
+ * the Widget type
+ */
+public abstract class WidgetRenderer<T, W extends Widget>
+ extends ComplexRenderer<T> {
+
+ @Override
+ public void init(RendererCellReference cell) {
+ // Implement if needed
+ }
+
+ /**
+ * Creates a widget to attach to a cell. The widgets will be attached to the
+ * cell after the cell element has been attached to DOM.
+ *
+ * @return widget to attach to a cell. All returned instances should be new
+ * widget instances without a parent.
+ */
+ public abstract W createWidget();
+
+ @Override
+ public void render(RendererCellReference cell, T data) {
+ W w = getWidget(cell.getElement());
+ assert w != null : "Widget not found in cell (" + cell.getColumn() + ","
+ + cell.getRow() + ")";
+ render(cell, data, w);
+ }
+
+ /**
+ * Renders a cell with a widget. This provides a way to update any
+ * information in the widget that is cell specific. Do not detach the Widget
+ * here, it will be done automatically by the Grid when the widget is no
+ * longer needed.
+ * <p>
+ * For optimal performance, work done in this method should be kept to a
+ * minimum since it will be called continuously while the user is scrolling.
+ * The renderer can use {@link Widget#setLayoutData(Object)} to store cell
+ * data that might be needed in e.g. event listeners.
+ *
+ * @param cell
+ * The cell to render. Note that the cell is a flyweight and
+ * should not be stored and used outside of this method as its
+ * contents will change.
+ * @param data
+ * the data of the cell
+ * @param widget
+ * the widget embedded in the cell
+ */
+ public abstract void render(RendererCellReference cell, T data, W widget);
+
+ /**
+ * Returns the widget contained inside the given cell element. Cannot be
+ * called for cells that do not contain a widget.
+ *
+ * @param e
+ * the element inside which to find a widget
+ * @return the widget inside the element
+ */
+ protected W getWidget(TableCellElement e) {
+ W w = getWidget(e, null);
+ assert w != null : "Widget not found inside cell";
+ return w;
+ }
+
+ /**
+ * Returns the widget contained inside the given cell element, or null if it
+ * is not an instance of the given class. Cannot be called for cells that do
+ * not contain a widget.
+ *
+ * @param e
+ * the element inside to find a widget
+ * @param klass
+ * the type of the widget to find
+ * @return the widget inside the element, or null if its type does not match
+ */
+ protected static <W extends Widget> W getWidget(TableCellElement e,
+ Class<W> klass) {
+ W w = WidgetUtil.findWidget(e.getFirstChildElement(), klass);
+ assert w == null || w.getElement() == e
+ .getFirstChildElement() : "Widget not found inside cell";
+ return w;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/JsniMousewheelHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/JsniMousewheelHandler.java
index 56b56ef40f..72d10edb36 100644
--- a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/JsniMousewheelHandler.java
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/JsniMousewheelHandler.java
@@ -19,7 +19,7 @@ import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Widget;
-import com.vaadin.client.widgets.Escalator;
+import com.vaadin.v7.client.widgets.Escalator;
/**
* A mousewheel handling class to get around the limits of
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Cell.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Cell.java
new file mode 100644
index 0000000000..33fe3e0ded
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Cell.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.google.gwt.dom.client.TableCellElement;
+
+/**
+ * Describes a cell
+ * <p>
+ * It's a representation of the element in a grid cell, and its row and column
+ * indices.
+ * <p>
+ * Unlike the {@link FlyweightRow}, an instance of {@link Cell} can be stored in
+ * a field.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Cell {
+
+ private final int row;
+
+ private final int column;
+
+ private final TableCellElement element;
+
+ /**
+ * Constructs a new {@link Cell}.
+ *
+ * @param row
+ * The index of the row
+ * @param column
+ * The index of the column
+ * @param element
+ * The cell element
+ */
+ public Cell(int row, int column, TableCellElement element) {
+ super();
+ this.row = row;
+ this.column = column;
+ this.element = element;
+ }
+
+ /**
+ * Returns the index of the row the cell resides in.
+ *
+ * @return the row index
+ *
+ */
+ public int getRow() {
+ return row;
+ }
+
+ /**
+ * Returns the index of the column the cell resides in.
+ *
+ * @return the column index
+ */
+ public int getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the element of the cell.
+ *
+ * @return the cell element
+ */
+ public TableCellElement getElement() {
+ return element;
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/ColumnConfiguration.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/ColumnConfiguration.java
new file mode 100644
index 0000000000..221ae96206
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/ColumnConfiguration.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import java.util.Map;
+
+import com.vaadin.v7.client.widgets.Escalator;
+
+/**
+ * A representation of the columns in an instance of {@link Escalator}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see Escalator#getColumnConfiguration()
+ */
+public interface ColumnConfiguration {
+
+ /**
+ * Removes columns at certain indices.
+ * <p>
+ * If any of the removed columns were frozen, the number of frozen columns
+ * will be reduced by the number of the removed columns that were frozen.
+ * <p>
+ * <em>Note:</em> This method simply removes the given columns, and does not
+ * do much of anything else. Especially if you have column spans, you
+ * probably need to run {@link #refreshColumns(int, int)} or
+ * {@link RowContainer#refreshRows(int, int)}
+ *
+ * @param index
+ * the index of the first column to be removed
+ * @param numberOfColumns
+ * the number of rows to remove, starting from {@code index}
+ * @throws IndexOutOfBoundsException
+ * if the entire range of removed columns is not currently
+ * present in the escalator
+ * @throws IllegalArgumentException
+ * if <code>numberOfColumns</code> is less than 1.
+ */
+ public void removeColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Adds columns at a certain index.
+ * <p>
+ * The new columns will be inserted between the column at the index, and the
+ * column before (an index of 0 means that the columns are inserted at the
+ * beginning). Therefore, the columns at the index and afterwards will be
+ * moved to the right.
+ * <p>
+ * The contents of the inserted columns will be queried from the respective
+ * cell renderers in the header, body and footer.
+ * <p>
+ * If there are frozen columns and the first added column is to the left of
+ * the last frozen column, the number of frozen columns will be increased by
+ * the number of inserted columns.
+ * <p>
+ * <em>Note:</em> Only the contents of the inserted columns will be
+ * rendered. If inserting new columns affects the contents of existing
+ * columns (e.g. you have column spans),
+ * {@link RowContainer#refreshRows(int, int)} or
+ * {@link #refreshColumns(int, int)} needs to be called as appropriate.
+ *
+ * @param index
+ * the index of the column before which new columns are inserted,
+ * or {@link #getColumnCount()} to add new columns at the end
+ * @param numberOfColumns
+ * the number of columns to insert after the <code>index</code>
+ * @throws IndexOutOfBoundsException
+ * if <code>index</code> is not an integer in the range
+ * <code>[0..{@link #getColumnCount()}]</code>
+ * @throws IllegalArgumentException
+ * if {@code numberOfColumns} is less than 1.
+ */
+ public void insertColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Returns the number of columns in the escalator.
+ *
+ * @return the number of columns in the escalator
+ */
+ public int getColumnCount();
+
+ /**
+ * Sets the number of leftmost columns that are not affected by horizontal
+ * scrolling.
+ *
+ * @param count
+ * the number of columns to freeze
+ *
+ * @throws IllegalArgumentException
+ * if the column count is &lt; 0 or &gt; the number of columns
+ *
+ */
+ public void setFrozenColumnCount(int count) throws IllegalArgumentException;
+
+ /**
+ * Get the number of leftmost columns that are not affected by horizontal
+ * scrolling.
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount();
+
+ /**
+ * Sets (or unsets) an explicit width for a column.
+ *
+ * @param index
+ * the index of the column for which to set a width
+ * @param px
+ * the number of pixels the indicated column should be, or a
+ * negative number to let the escalator decide
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public void setColumnWidth(int index, double px)
+ throws IllegalArgumentException;
+
+ /**
+ * Returns the user-defined width of a column.
+ *
+ * @param index
+ * the index of the column for which to retrieve the width
+ * @return the column's width in pixels, or a negative number if the width
+ * is implicitly decided by the escalator
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public double getColumnWidth(int index) throws IllegalArgumentException;
+
+ /**
+ * Sets widths for a set of columns.
+ *
+ * @param indexWidthMap
+ * a map from column index to its respective width to be set. If
+ * the given width for a column index is negative, the column is
+ * resized-to-fit.
+ * @throws IllegalArgumentException
+ * if {@code indexWidthMap} is {@code null}
+ * @throws IllegalArgumentException
+ * if any column index in {@code indexWidthMap} is invalid
+ * @throws NullPointerException
+ * If any value in the map is <code>null</code>
+ */
+ public void setColumnWidths(Map<Integer, Double> indexWidthMap)
+ throws IllegalArgumentException;
+
+ /**
+ * Returns the actual width of a column.
+ *
+ * @param index
+ * the index of the column for which to retrieve the width
+ * @return the column's actual width in pixels
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public double getColumnWidthActual(int index)
+ throws IllegalArgumentException;
+
+ /**
+ * Refreshes a range of rows in the current row containers in each Escalator
+ * section.
+ * <p>
+ * The data for the refreshed columns is queried from the current cell
+ * renderer.
+ *
+ * @param index
+ * the index of the first row that will be updated
+ * @param numberOfRows
+ * the number of rows to update, starting from the index
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[index..(index+numberOfColumns)]</code> is not an
+ * existing column index.
+ * @throws IllegalArgumentException
+ * if {@code numberOfColumns} is less than 1.
+ * @see RowContainer#setEscalatorUpdater(EscalatorUpdater)
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ */
+ public void refreshColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/EscalatorUpdater.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/EscalatorUpdater.java
new file mode 100644
index 0000000000..4319d98c50
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/EscalatorUpdater.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.vaadin.v7.client.widgets.Escalator;
+
+/**
+ * An interface that allows client code to define how a certain row in Escalator
+ * will be displayed. The contents of an escalator's header, body and footer are
+ * rendered by their respective updaters.
+ * <p>
+ * The updater is responsible for internally handling all remote communication,
+ * should the displayed data need to be fetched remotely.
+ * <p>
+ * This has a similar function to {@link Grid Grid's} {@link Renderer Renderers}
+ * , although they operate on different abstraction levels.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see RowContainer#setEscalatorUpdater(EscalatorUpdater)
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ * @see Renderer
+ */
+public interface EscalatorUpdater {
+
+ /**
+ * An {@link EscalatorUpdater} that doesn't render anything.
+ */
+ public static final EscalatorUpdater NULL = new EscalatorUpdater() {
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ // NOOP
+ }
+
+ @Override
+ public void preAttach(final Row row,
+ final Iterable<FlyweightCell> cellsToAttach) {
+ // NOOP
+
+ }
+
+ @Override
+ public void postAttach(final Row row,
+ final Iterable<FlyweightCell> attachedCells) {
+ // NOOP
+ }
+
+ @Override
+ public void preDetach(final Row row,
+ final Iterable<FlyweightCell> cellsToDetach) {
+ // NOOP
+ }
+
+ @Override
+ public void postDetach(final Row row,
+ final Iterable<FlyweightCell> detachedCells) {
+ // NOOP
+ }
+ };
+
+ /**
+ * Renders a row contained in a row container.
+ * <p>
+ * <em>Note:</em> If rendering of cells is deferred (e.g. because
+ * asynchronous data retrieval), this method is responsible for explicitly
+ * displaying some placeholder data (empty content is valid). Because the
+ * cells (and rows) in an escalator are recycled, failing to reset a cell's
+ * presentation will lead to wrong data being displayed in the escalator.
+ * <p>
+ * For performance reasons, the escalator will never autonomously clear any
+ * data in a cell.
+ *
+ * @param row
+ * Information about the row that is being updated.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param cellsToUpdate
+ * A collection of cells that need to be updated. <em>Note:</em>
+ * You should neither store nor reuse the reference to the
+ * iterable, nor to the individual cells.
+ */
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate);
+
+ /**
+ * Called before attaching new cells to the escalator.
+ *
+ * @param row
+ * Information about the row to which the cells will be added.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param cellsToAttach
+ * A collection of cells that are about to be attached.
+ * <em>Note:</em> You should neither store nor reuse the
+ * reference to the iterable, nor to the individual cells.
+ *
+ */
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach);
+
+ /**
+ * Called after attaching new cells to the escalator.
+ *
+ * @param row
+ * Information about the row to which the cells were added.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param attachedCells
+ * A collection of cells that were attached. <em>Note:</em> You
+ * should neither store nor reuse the reference to the iterable,
+ * nor to the individual cells.
+ *
+ */
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells);
+
+ /**
+ * Called before detaching cells from the escalator.
+ *
+ * @param row
+ * Information about the row from which the cells will be
+ * removed. <em>Note:</em> You should not store nor reuse this
+ * reference.
+ * @param cellsToAttach
+ * A collection of cells that are about to be detached.
+ * <em>Note:</em> You should neither store nor reuse the
+ * reference to the iterable, nor to the individual cells.
+ *
+ */
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach);
+
+ /**
+ * Called after detaching cells from the escalator.
+ *
+ * @param row
+ * Information about the row from which the cells were removed.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param attachedCells
+ * A collection of cells that were detached. <em>Note:</em> You
+ * should neither store nor reuse the reference to the iterable,
+ * nor to the individual cells.
+ *
+ */
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells);
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/FlyweightCell.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/FlyweightCell.java
new file mode 100644
index 0000000000..d77b94a352
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/FlyweightCell.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import java.util.List;
+
+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.vaadin.v7.client.widget.escalator.FlyweightRow.CellIterator;
+import com.vaadin.v7.client.widgets.Escalator;
+
+/**
+ * A {@link FlyweightCell} represents a cell in the {@link Grid} or
+ * {@link Escalator} at a certain point in time.
+ *
+ * <p>
+ * Since the {@link FlyweightCell} 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.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class FlyweightCell {
+ public static final String COLSPAN_ATTR = "colSpan";
+
+ private final int column;
+ private final FlyweightRow row;
+
+ private TableCellElement element = null;
+ private CellIterator currentIterator = null;
+
+ public FlyweightCell(final FlyweightRow row, final int column) {
+ this.row = row;
+ this.column = column;
+ }
+
+ /**
+ * Returns the row index of the cell
+ */
+ public int getRow() {
+ assertSetup();
+ return row.getRow();
+ }
+
+ /**
+ * Returns the column index of the cell
+ */
+ public int getColumn() {
+ assertSetup();
+ return column;
+ }
+
+ /**
+ * Returns the element of the cell. Can be either a <code>TD</code> element
+ * or a <code>TH</code> element.
+ */
+ public TableCellElement getElement() {
+ assertSetup();
+ return element;
+ }
+
+ /**
+ * Return the colspan attribute of the element of the cell.
+ */
+ public int getColSpan() {
+ assertSetup();
+ return element.getPropertyInt(COLSPAN_ATTR);
+ }
+
+ /**
+ * Sets the DOM element for this FlyweightCell, either a <code>TD</code> or
+ * a <code>TH</code>. It is the caller's responsibility to actually insert
+ * the given element to the document when needed.
+ *
+ * @param element
+ * the element corresponding to this cell, cannot be null
+ */
+ public void setElement(TableCellElement element) {
+ assert element != null;
+ assertSetup();
+ this.element = element;
+ }
+
+ void setup(final CellIterator iterator) {
+ currentIterator = iterator;
+
+ if (iterator.areCellsAttached()) {
+ final TableCellElement e = row.getElement().getCells()
+ .getItem(column);
+
+ assert e != null : "Cell " + column + " for logical row "
+ + row.getRow() + " doesn't exist in the DOM!";
+
+ e.setPropertyInt(COLSPAN_ATTR, 1);
+ if (row.getColumnWidth(column) >= 0) {
+ e.getStyle().setWidth(row.getColumnWidth(column), Unit.PX);
+ }
+ e.getStyle().clearDisplay();
+ setElement(e);
+ }
+ }
+
+ /**
+ * Tear down the state of the Cell.
+ * <p>
+ * This is an internal check method, to prevent retrieving uninitialized
+ * data by calling {@link #getRow()}, {@link #getColumn()} or
+ * {@link #getElement()} at an improper time.
+ * <p>
+ * This should only be used with asserts ("
+ * <code>assert flyweightCell.teardown()</code> ") so that the code is never
+ * run when asserts aren't enabled.
+ *
+ * @return always <code>true</code>
+ * @see FlyweightRow#teardown()
+ */
+ boolean teardown() {
+ currentIterator = null;
+ element = null;
+ return true;
+ }
+
+ /**
+ * Asserts that the flyweight cell has properly been set up before trying to
+ * access any of its data.
+ */
+ private void assertSetup() {
+ assert currentIterator != null : "FlyweightCell was not properly "
+ + "initialized. This is either a bug in Grid/Escalator "
+ + "or a Cell reference has been stored and reused "
+ + "inappropriately.";
+ }
+
+ public void setColSpan(final int numberOfCells) {
+ if (numberOfCells < 1) {
+ throw new IllegalArgumentException(
+ "Number of cells should be more than 0");
+ }
+
+ /*-
+ * This will default to 1 if unset, as per DOM specifications:
+ * http://www.w3.org/TR/html5/tabular-data.html#attributes-common-to-td-and-th-elements
+ */
+ final int prevColSpan = getElement().getPropertyInt(COLSPAN_ATTR);
+ if (numberOfCells == 1 && prevColSpan == 1) {
+ return;
+ }
+
+ getElement().setPropertyInt(COLSPAN_ATTR, numberOfCells);
+ adjustCellWidthForSpan(numberOfCells);
+ hideOrRevealAdjacentCellElements(numberOfCells, prevColSpan);
+ currentIterator.setSkipNext(numberOfCells - 1);
+ }
+
+ private void adjustCellWidthForSpan(final int numberOfCells) {
+ final int cellsToTheRight = currentIterator
+ .rawPeekNext(numberOfCells - 1).size();
+
+ final double selfWidth = row.getColumnWidth(column);
+ double widthsOfColumnsToTheRight = 0;
+ for (int i = 0; i < cellsToTheRight; i++) {
+ widthsOfColumnsToTheRight += row.getColumnWidth(column + i + 1);
+ }
+ getElement().getStyle().setWidth(selfWidth + widthsOfColumnsToTheRight,
+ Unit.PX);
+ }
+
+ private void hideOrRevealAdjacentCellElements(final int numberOfCells,
+ final int prevColSpan) {
+ final int affectedCellsNumber = Math.max(prevColSpan, numberOfCells);
+ final List<FlyweightCell> affectedCells = currentIterator
+ .rawPeekNext(affectedCellsNumber - 1);
+ if (prevColSpan < numberOfCells) {
+ for (int i = 0; i < affectedCells.size(); i++) {
+ affectedCells.get(prevColSpan + i - 1).getElement().getStyle()
+ .setDisplay(Display.NONE);
+ }
+ } else if (prevColSpan > numberOfCells) {
+ for (int i = 0; i < affectedCells.size(); i++) {
+ affectedCells.get(numberOfCells + i - 1).getElement().getStyle()
+ .clearDisplay();
+ }
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/FlyweightRow.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/FlyweightRow.java
new file mode 100644
index 0000000000..7f6e9d4049
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/FlyweightRow.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.dom.client.TableRowElement;
+import com.vaadin.v7.client.widgets.Escalator;
+
+/**
+ * An internal implementation of the {@link Row} interface.
+ * <p>
+ * There is only one instance per Escalator. This is designed to be re-used when
+ * rendering rows.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see Escalator.AbstractRowContainer#refreshRow(Node, int)
+ */
+public class FlyweightRow implements Row {
+
+ static class CellIterator implements Iterator<FlyweightCell> {
+ /** A defensive copy of the cells in the current row. */
+ private final ArrayList<FlyweightCell> cells;
+ private final boolean cellsAttached;
+ private int cursor = 0;
+ private int skipNext = 0;
+
+ /**
+ * Creates a new iterator of attached flyweight cells. A cell is
+ * attached if it has a corresponding {@link FlyweightCell#getElement()
+ * DOM element} attached to the row element.
+ *
+ * @param cells
+ * the collection of cells to iterate
+ */
+ public static CellIterator attached(
+ final Collection<FlyweightCell> cells) {
+ return new CellIterator(cells, true);
+ }
+
+ /**
+ * Creates a new iterator of unattached flyweight cells. A cell is
+ * unattached if it does not have a corresponding
+ * {@link FlyweightCell#getElement() DOM element} attached to the row
+ * element.
+ *
+ * @param cells
+ * the collection of cells to iterate
+ */
+ public static CellIterator unattached(
+ final Collection<FlyweightCell> cells) {
+ return new CellIterator(cells, false);
+ }
+
+ private CellIterator(final Collection<FlyweightCell> cells,
+ final boolean attached) {
+ this.cells = new ArrayList<FlyweightCell>(cells);
+ cellsAttached = attached;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return cursor + skipNext < cells.size();
+ }
+
+ @Override
+ public FlyweightCell next() {
+ // if we needed to skip some cells since the last invocation.
+ for (int i = 0; i < skipNext; i++) {
+ cells.remove(cursor);
+ }
+ skipNext = 0;
+
+ final FlyweightCell cell = cells.get(cursor++);
+ cell.setup(this);
+ return cell;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException(
+ "Cannot remove cells via iterator");
+ }
+
+ /**
+ * Sets the number of cells to skip when {@link #next()} is called the
+ * next time. Cell hiding is also handled eagerly in this method.
+ *
+ * @param colspan
+ * the number of cells to skip on next invocation of
+ * {@link #next()}
+ */
+ public void setSkipNext(final int colspan) {
+ assert colspan > 0 : "Number of cells didn't make sense: "
+ + colspan;
+ skipNext = colspan;
+ }
+
+ /**
+ * Gets the next <code>n</code> cells in the iterator, ignoring any
+ * possibly spanned cells.
+ *
+ * @param n
+ * the number of next cells to retrieve
+ * @return A list of next <code>n</code> cells, or less if there aren't
+ * enough cells to retrieve
+ */
+ public List<FlyweightCell> rawPeekNext(final int n) {
+ final int from = Math.min(cursor, cells.size());
+ final int to = Math.min(cursor + n, cells.size());
+ List<FlyweightCell> nextCells = cells.subList(from, to);
+ for (FlyweightCell cell : nextCells) {
+ cell.setup(this);
+ }
+ return nextCells;
+ }
+
+ public boolean areCellsAttached() {
+ return cellsAttached;
+ }
+ }
+
+ private static final int BLANK = Integer.MIN_VALUE;
+
+ private int row;
+ private TableRowElement element;
+ private double[] columnWidths = null;
+ private final List<FlyweightCell> cells = new ArrayList<FlyweightCell>();
+
+ public void setup(final TableRowElement e, final int row,
+ double[] columnWidths) {
+ element = e;
+ this.row = row;
+ this.columnWidths = columnWidths;
+ }
+
+ /**
+ * Tear down the state of the Row.
+ * <p>
+ * This is an internal check method, to prevent retrieving uninitialized
+ * data by calling {@link #getRow()}, {@link #getElement()} or
+ * {@link #getCells()} at an improper time.
+ * <p>
+ * This should only be used with asserts ("
+ * <code>assert flyweightRow.teardown()</code> ") so that the code is never
+ * run when asserts aren't enabled.
+ *
+ * @return always <code>true</code>
+ */
+ public boolean teardown() {
+ element = null;
+ row = BLANK;
+ columnWidths = null;
+ for (final FlyweightCell cell : cells) {
+ assert cell.teardown();
+ }
+ return true;
+ }
+
+ @Override
+ public int getRow() {
+ assertSetup();
+ return row;
+ }
+
+ @Override
+ public TableRowElement getElement() {
+ assertSetup();
+ return element;
+ }
+
+ public void addCells(final int index, final int numberOfColumns) {
+ for (int i = 0; i < numberOfColumns; i++) {
+ final int col = index + i;
+ cells.add(col, new FlyweightCell(this, col));
+ }
+ updateRestOfCells(index + numberOfColumns);
+ }
+
+ public void removeCells(final int index, final int numberOfColumns) {
+ cells.subList(index, index + numberOfColumns).clear();
+ updateRestOfCells(index);
+ }
+
+ private void updateRestOfCells(final int startPos) {
+ // update the column number for the cells to the right
+ for (int col = startPos; col < cells.size(); col++) {
+ cells.set(col, new FlyweightCell(this, col));
+ }
+ }
+
+ /**
+ * Returns flyweight cells for the client code to render. The cells get
+ * their associated {@link FlyweightCell#getElement() elements} from the row
+ * element.
+ * <p>
+ * Precondition: each cell has a corresponding element in the row
+ *
+ * @return an iterable of flyweight cells
+ *
+ * @see #setup(Element, int, int[])
+ * @see #teardown()
+ */
+ public Iterable<FlyweightCell> getCells() {
+ return getCells(0, cells.size());
+ }
+
+ /**
+ * Returns a subrange of flyweight cells for the client code to render. The
+ * cells get their associated {@link FlyweightCell#getElement() elements}
+ * from the row element.
+ * <p>
+ * Precondition: each cell has a corresponding element in the row
+ *
+ * @param offset
+ * the index of the first cell to return
+ * @param numberOfCells
+ * the number of cells to return
+ * @return an iterable of flyweight cells
+ */
+ public Iterable<FlyweightCell> getCells(final int offset,
+ final int numberOfCells) {
+ assertSetup();
+ assert offset >= 0 && offset + numberOfCells <= cells
+ .size() : "Invalid range of cells";
+ return new Iterable<FlyweightCell>() {
+ @Override
+ public Iterator<FlyweightCell> iterator() {
+ return CellIterator.attached(
+ cells.subList(offset, offset + numberOfCells));
+ }
+ };
+ }
+
+ /**
+ * Returns a subrange of unattached flyweight cells. Unattached cells do not
+ * have {@link FlyweightCell#getElement() elements} associated. Note that
+ * FlyweightRow does not keep track of whether cells in actuality have
+ * corresponding DOM elements or not; it is the caller's responsibility to
+ * invoke this method with correct parameters.
+ * <p>
+ * Precondition: the range [offset, offset + numberOfCells) must be valid
+ *
+ * @param offset
+ * the index of the first cell to return
+ * @param numberOfCells
+ * the number of cells to return
+ * @return an iterable of flyweight cells
+ */
+ public Iterable<FlyweightCell> getUnattachedCells(final int offset,
+ final int numberOfCells) {
+ assertSetup();
+ assert offset >= 0 && offset + numberOfCells <= cells
+ .size() : "Invalid range of cells";
+ return new Iterable<FlyweightCell>() {
+ @Override
+ public Iterator<FlyweightCell> iterator() {
+ return CellIterator.unattached(
+ cells.subList(offset, offset + numberOfCells));
+ }
+ };
+ }
+
+ /**
+ * Asserts that the flyweight row has properly been set up before trying to
+ * access any of its data.
+ */
+ private void assertSetup() {
+ assert element != null && row != BLANK
+ && columnWidths != null : "Flyweight row was not "
+ + "properly initialized. Make sure the setup-method is "
+ + "called before retrieving data. This is either a bug "
+ + "in Escalator, or the instance of the flyweight row "
+ + "has been stored and accessed.";
+ }
+
+ double getColumnWidth(int column) {
+ assertSetup();
+ return columnWidths[column];
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/PositionFunction.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/PositionFunction.java
new file mode 100644
index 0000000000..39cbdb9be0
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/PositionFunction.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
+
+/**
+ * A functional interface that can be used for positioning elements in the DOM.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface PositionFunction {
+ /**
+ * A position function using "transform: translate3d(x,y,z)" to position
+ * elements in the DOM.
+ */
+ public static class Translate3DPosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("transform",
+ "translate3d(" + x + "px, " + y + "px, 0)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("transform");
+ }
+ }
+
+ /**
+ * A position function using "transform: translate(x,y)" to position
+ * elements in the DOM.
+ */
+ public static class TranslatePosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("transform",
+ "translate(" + x + "px," + y + "px)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("transform");
+ }
+ }
+
+ /**
+ * A position function using "-webkit-transform: translate3d(x,y,z)" to
+ * position elements in the DOM.
+ */
+ public static class WebkitTranslate3DPosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("webkitTransform",
+ "translate3d(" + x + "px," + y + "px,0)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("webkitTransform");
+ }
+ }
+
+ /**
+ * A position function using "left: x" and "top: y" to position elements in
+ * the DOM.
+ */
+ public static class AbsolutePosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setLeft(x, Unit.PX);
+ e.getStyle().setTop(y, Unit.PX);
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearLeft();
+ e.getStyle().clearTop();
+ }
+ }
+
+ /**
+ * Position an element in an (x,y) coordinate system in the DOM.
+ *
+ * @param e
+ * the element to position. Never <code>null</code>.
+ * @param x
+ * the x coordinate, in pixels
+ * @param y
+ * the y coordinate, in pixels
+ */
+ void set(Element e, double x, double y);
+
+ /**
+ * Resets any previously applied positioning, clearing the used style
+ * attributes.
+ *
+ * @param e
+ * the element for which to reset the positioning
+ */
+ void reset(Element e);
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Row.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Row.java
new file mode 100644
index 0000000000..625de2f561
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Row.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.google.gwt.dom.client.TableRowElement;
+import com.vaadin.v7.client.widgets.Escalator;
+
+/**
+ * A representation of a row in an {@link Escalator}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface Row {
+ /**
+ * Gets the row index.
+ *
+ * @return the row index
+ */
+ public int getRow();
+
+ /**
+ * Gets the root element for this row.
+ * <p>
+ * The {@link EscalatorUpdater} may update the class names of the element
+ * and add inline styles, but may not modify the contained DOM structure.
+ * <p>
+ * If you wish to modify the cells within this row element, access them via
+ * the <code>List&lt;{@link Cell}&gt;</code> objects passed in to
+ * {@code EscalatorUpdater.updateCells(Row, List)}
+ *
+ * @return the root element of the row
+ */
+ public TableRowElement getElement();
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowContainer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowContainer.java
new file mode 100644
index 0000000000..b1ff0e7025
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowContainer.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.TableSectionElement;
+import com.vaadin.v7.client.widgets.Escalator;
+
+/**
+ * A representation of the rows in each of the sections (header, body and
+ * footer) in an {@link com.vaadin.v7.client.widgets.Escalator}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see com.vaadin.v7.client.widgets.Escalator#getHeader()
+ * @see com.vaadin.v7.client.widgets.Escalator#getBody()
+ * @see com.vaadin.v7.client.widgets.Escalator#getFooter()
+ * @see SpacerContainer
+ */
+public interface RowContainer {
+
+ /**
+ * The row container for the body section in an
+ * {@link com.vaadin.v7.client.widgets.Escalator}.
+ * <p>
+ * The body section can contain both rows and spacers.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ * @see com.vaadin.v7.client.widgets.Escalator#getBody()
+ */
+ public interface BodyRowContainer extends RowContainer {
+
+ /**
+ * Marks a spacer and its height.
+ * <p>
+ * If a spacer is already registered with the given row index, that
+ * spacer will be updated with the given height.
+ * <p>
+ * <em>Note:</em> The row index for a spacer will change if rows are
+ * inserted or removed above the current position. Spacers will also be
+ * removed alongside their associated rows
+ *
+ * @param rowIndex
+ * the row index for the spacer to modify. The affected
+ * spacer is underneath the given index. Use -1 to insert a
+ * spacer before the first row
+ * @param height
+ * the pixel height of the spacer. If {@code height} is
+ * negative, the affected spacer (if exists) will be removed
+ * @throws IllegalArgumentException
+ * if {@code rowIndex} is not a valid row index
+ * @see #insertRows(int, int)
+ * @see #removeRows(int, int)
+ */
+ void setSpacer(int rowIndex, double height)
+ throws IllegalArgumentException;
+
+ /**
+ * Sets a new spacer updater.
+ * <p>
+ * Spacers that are currently visible will be updated, i.e.
+ * {@link SpacerUpdater#destroy(Spacer) destroyed} with the previous
+ * one, and {@link SpacerUpdater#init(Spacer) initialized} with the new
+ * one.
+ *
+ * @param spacerUpdater
+ * the new spacer updater
+ * @throws IllegalArgumentException
+ * if {@code spacerUpdater} is {@code null}
+ */
+ void setSpacerUpdater(SpacerUpdater spacerUpdater)
+ throws IllegalArgumentException;
+
+ /**
+ * Gets the spacer updater currently in use.
+ * <p>
+ * {@link SpacerUpdater#NULL} is the default.
+ *
+ * @return the spacer updater currently in use. Never <code>null</code>
+ */
+ SpacerUpdater getSpacerUpdater();
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Any spacers underneath {@code index} will be offset and "pushed"
+ * down. This also modifies the row index they are associated with.
+ */
+ @Override
+ public void insertRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Any spacers underneath {@code index} will be offset and "pulled" up.
+ * This also modifies the row index they are associated with. Any
+ * spacers in the removed range will also be closed and removed.
+ */
+ @Override
+ public void removeRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+ }
+
+ /**
+ * An arbitrary pixel height of a row, before any autodetection for the row
+ * height has been made.
+ */
+ public static final double INITIAL_DEFAULT_ROW_HEIGHT = 20;
+
+ /**
+ * Returns the current {@link EscalatorUpdater} used to render cells.
+ *
+ * @return the current escalator updater
+ */
+ public EscalatorUpdater getEscalatorUpdater();
+
+ /**
+ * Sets the {@link EscalatorUpdater} to use when displaying data in the
+ * escalator.
+ *
+ * @param escalatorUpdater
+ * the escalator updater to use to render cells. May not be
+ * <code>null</code>
+ * @throws IllegalArgumentException
+ * if {@code cellRenderer} is <code>null</code>
+ * @see EscalatorUpdater#NULL
+ */
+ public void setEscalatorUpdater(EscalatorUpdater escalatorUpdater)
+ throws IllegalArgumentException;
+
+ /**
+ * Removes rows at a certain index in the current row container.
+ *
+ * @param index
+ * the index of the first row to be removed
+ * @param numberOfRows
+ * the number of rows to remove, starting from the index
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[index..(index+numberOfRows)]</code> is not an existing
+ * row index
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void removeRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Adds rows at a certain index in this row container.
+ * <p>
+ * The new rows will be inserted between the row at the index, and the row
+ * before (an index of 0 means that the rows are inserted at the beginning).
+ * Therefore, the rows currently at the index and afterwards will be moved
+ * downwards.
+ * <p>
+ * The contents of the inserted rows will subsequently be queried from the
+ * escalator updater.
+ * <p>
+ * <em>Note:</em> Only the contents of the inserted rows will be rendered.
+ * If inserting new rows affects the contents of existing rows,
+ * {@link #refreshRows(int, int)} needs to be called for those rows
+ * separately.
+ *
+ * @param index
+ * the index of the row before which new rows are inserted, or
+ * {@link #getRowCount()} to add rows at the end
+ * @param numberOfRows
+ * the number of rows to insert after the <code>index</code>
+ * @see #setEscalatorUpdater(EscalatorUpdater)
+ * @throws IndexOutOfBoundsException
+ * if <code>index</code> is not an integer in the range
+ * <code>[0..{@link #getRowCount()}]</code>
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void insertRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Refreshes a range of rows in the current row container.
+ * <p>
+ * The data for the refreshed rows is queried from the current cell
+ * renderer.
+ *
+ * @param index
+ * the index of the first row that will be updated
+ * @param numberOfRows
+ * the number of rows to update, starting from the index
+ * @see #setEscalatorUpdater(EscalatorUpdater)
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[index..(index+numberOfColumns)]</code> is not an
+ * existing column index.
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void refreshRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Gets the number of rows in the current row container.
+ *
+ * @return the number of rows in the current row container
+ */
+ public int getRowCount();
+
+ /**
+ * The default height of the rows in this RowContainer.
+ *
+ * @param px
+ * the default height in pixels of the rows in this RowContainer
+ * @throws IllegalArgumentException
+ * if <code>px &lt; 1</code>
+ * @see #getDefaultRowHeight()
+ */
+ public void setDefaultRowHeight(double px) throws IllegalArgumentException;
+
+ /**
+ * Returns the default height of the rows in this RowContainer.
+ * <p>
+ * This value will be equal to {@link #INITIAL_DEFAULT_ROW_HEIGHT} if the
+ * {@link Escalator} has not yet had a chance to autodetect the row height,
+ * or no explicit value has yet given via {@link #setDefaultRowHeight(int)}
+ *
+ * @return the default height of the rows in this RowContainer, in pixels
+ * @see #setDefaultRowHeight(int)
+ */
+ public double getDefaultRowHeight();
+
+ /**
+ * Returns the cell object which contains information about the cell the
+ * element is in.
+ *
+ * @param element
+ * The element to get the cell for. If element is not present in
+ * row container then <code>null</code> is returned.
+ *
+ * @return the cell of the element, or <code>null</code> if element is not
+ * present in the {@link RowContainer}.
+ */
+ public Cell getCell(Element element);
+
+ /**
+ * Gets the row element with given logical index. For lazy loaded containers
+ * such as Escalators BodyRowContainer visibility should be checked before
+ * calling this function. See {@link Escalator#getVisibleRowRange()}.
+ *
+ * @param index
+ * the logical index of the element to retrieve
+ * @return the element at position {@code index}
+ * @throws IndexOutOfBoundsException
+ * if {@code index} is not valid within container
+ * @throws IllegalStateException
+ * if {@code index} is currently not available in the DOM
+ */
+ public TableRowElement getRowElement(int index)
+ throws IndexOutOfBoundsException, IllegalStateException;
+
+ /**
+ * Returns the root element of RowContainer
+ *
+ * @return RowContainer root element
+ */
+ public TableSectionElement getElement();
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeEvent.java
new file mode 100644
index 0000000000..d5c0001594
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeEvent.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.shared.ui.grid.Range;
+
+/**
+ * Event fired when the range of visible rows changes e.g. because of scrolling.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class RowVisibilityChangeEvent
+ extends GwtEvent<RowVisibilityChangeHandler> {
+ /**
+ * The type of this event.
+ */
+ public static final Type<RowVisibilityChangeHandler> TYPE = new Type<RowVisibilityChangeHandler>();
+
+ private final Range visibleRows;
+
+ /**
+ * Creates a new row visibility change event
+ *
+ * @param firstVisibleRow
+ * the index of the first visible row
+ * @param visibleRowCount
+ * the number of visible rows
+ */
+ public RowVisibilityChangeEvent(int firstVisibleRow, int visibleRowCount) {
+ visibleRows = Range.withLength(firstVisibleRow, visibleRowCount);
+ }
+
+ /**
+ * Gets the index of the first row that is at least partially visible.
+ *
+ * @return the index of the first visible row
+ */
+ public int getFirstVisibleRow() {
+ return visibleRows.getStart();
+ }
+
+ /**
+ * Gets the number of at least partially visible rows.
+ *
+ * @return the number of visible rows
+ */
+ public int getVisibleRowCount() {
+ return visibleRows.length();
+ }
+
+ /**
+ * Gets the range of visible rows.
+ *
+ * @since 7.6
+ * @return the visible rows
+ */
+ public Range getVisibleRowRange() {
+ return visibleRows;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.event.shared.GwtEvent#getAssociatedType()
+ */
+ @Override
+ public Type<RowVisibilityChangeHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.shared.GwtEvent#dispatch(com.google.gwt.event.shared
+ * .EventHandler)
+ */
+ @Override
+ protected void dispatch(RowVisibilityChangeHandler handler) {
+ handler.onRowVisibilityChange(this);
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeHandler.java
new file mode 100644
index 0000000000..2a52af0269
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Event handler that gets notified when the range of visible rows changes e.g.
+ * because of scrolling.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface RowVisibilityChangeHandler extends EventHandler {
+
+ /**
+ * Called when the range of visible rows changes e.g. because of scrolling.
+ *
+ * @param event
+ * the row visibility change event describing the change
+ */
+ void onRowVisibilityChange(RowVisibilityChangeEvent event);
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/ScrollbarBundle.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/ScrollbarBundle.java
new file mode 100644
index 0000000000..bdd4240818
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/ScrollbarBundle.java
@@ -0,0 +1,869 @@
+/*
+ * Copyright 2000-2016 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.v7.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.v7.client.widget.grid.events.ScrollEvent;
+import com.vaadin.v7.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 (" + 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;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Spacer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Spacer.java
new file mode 100644
index 0000000000..3942f2c6ed
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/Spacer.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.google.gwt.dom.client.Element;
+
+/**
+ * A representation of a spacer element in a
+ * {@link com.vaadin.v7.client.widget.escalator.RowContainer.BodyRowContainer}.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ */
+public interface Spacer {
+
+ /**
+ * Gets the root element for the spacer content.
+ *
+ * @return the root element for the spacer content
+ */
+ Element getElement();
+
+ /**
+ * Gets the decorative element for this spacer.
+ */
+ Element getDecoElement();
+
+ /**
+ * Gets the row index.
+ *
+ * @return the row index.
+ */
+ int getRow();
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/SpacerUpdater.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/SpacerUpdater.java
new file mode 100644
index 0000000000..51d5ea7806
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/SpacerUpdater.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator;
+
+import com.vaadin.v7.client.widget.escalator.RowContainer.BodyRowContainer;
+
+/**
+ * An interface that handles the display of content for spacers.
+ * <p>
+ * The updater is responsible for making sure all elements are properly
+ * constructed and cleaned up.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ * @see Spacer
+ * @see BodyRowContainer
+ */
+public interface SpacerUpdater {
+
+ /** A spacer updater that does nothing. */
+ public static final SpacerUpdater NULL = new SpacerUpdater() {
+ @Override
+ public void init(Spacer spacer) {
+ // NOOP
+ }
+
+ @Override
+ public void destroy(Spacer spacer) {
+ // NOOP
+ }
+ };
+
+ /**
+ * Called whenever a spacer should be initialized with content.
+ *
+ * @param spacer
+ * the spacer reference that should be initialized
+ */
+ void init(Spacer spacer);
+
+ /**
+ * Called whenever a spacer should be cleaned.
+ * <p>
+ * The structure to clean up is the same that has been constructed by
+ * {@link #init(Spacer)}.
+ *
+ * @param spacer
+ * the spacer reference that should be destroyed
+ */
+ void destroy(Spacer spacer);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/events/RowHeightChangedEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/events/RowHeightChangedEvent.java
new file mode 100644
index 0000000000..c883a26716
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/events/RowHeightChangedEvent.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * Event fired when the row height changed in the Escalator's header, body or
+ * footer.
+ *
+ * @since 7.7
+ * @author Vaadin Ltd
+ */
+public class RowHeightChangedEvent extends GwtEvent<RowHeightChangedHandler> {
+
+ /**
+ * Handler type.
+ */
+ public final static Type<RowHeightChangedHandler> TYPE = new Type<RowHeightChangedHandler>();
+
+ public static final Type<RowHeightChangedHandler> getType() {
+ return TYPE;
+ }
+
+ @Override
+ public Type<RowHeightChangedHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(RowHeightChangedHandler handler) {
+ handler.onRowHeightChanged(this);
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/events/RowHeightChangedHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/events/RowHeightChangedHandler.java
new file mode 100644
index 0000000000..94c9e895ae
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/events/RowHeightChangedHandler.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.escalator.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Event handler for a row height changed event.
+ *
+ * @since 7.7
+ * @author Vaadin Ltd
+ */
+public interface RowHeightChangedHandler extends EventHandler {
+
+ /**
+ * A row height changed event, fired by Escalator when the header, body or
+ * footer row height has changed.
+ *
+ * @param event
+ * Row height changed event
+ */
+ public void onRowHeightChanged(RowHeightChangedEvent event);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/AutoScroller.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/AutoScroller.java
new file mode 100644
index 0000000000..500d616bc4
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/AutoScroller.java
@@ -0,0 +1,647 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+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.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.vaadin.client.WidgetUtil;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * A class for handling automatic scrolling vertically / horizontally in the
+ * Grid when the cursor is close enough the edge of the body of the grid,
+ * depending on the scroll direction chosen.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ */
+public class AutoScroller {
+
+ /**
+ * Callback that notifies when the cursor is on top of a new row or column
+ * because of the automatic scrolling.
+ */
+ public interface AutoScrollerCallback {
+
+ /**
+ * Triggered when doing automatic scrolling.
+ * <p>
+ * Because the auto scroller currently only supports scrolling in one
+ * axis, this method is used for both vertical and horizontal scrolling.
+ *
+ * @param scrollDiff
+ * the amount of pixels that have been auto scrolled since
+ * last call
+ */
+ void onAutoScroll(int scrollDiff);
+
+ /**
+ * Triggered when the grid scroll has reached the minimum scroll
+ * position. Depending on the scroll axis, either scrollLeft or
+ * scrollTop is 0.
+ */
+ void onAutoScrollReachedMin();
+
+ /**
+ * Triggered when the grid scroll has reached the max scroll position.
+ * Depending on the scroll axis, either scrollLeft or scrollTop is at
+ * its maximum value.
+ */
+ void onAutoScrollReachedMax();
+ }
+
+ public enum ScrollAxis {
+ VERTICAL, HORIZONTAL
+ }
+
+ /** The maximum number of pixels per second to autoscroll. */
+ private static final int SCROLL_TOP_SPEED_PX_SEC = 500;
+
+ /**
+ * The minimum area where the grid doesn't scroll while the pointer is
+ * pressed.
+ */
+ private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50;
+
+ /** The size of the autoscroll area, both top/left and bottom/right. */
+ private int scrollAreaPX = 100;
+
+ /**
+ * This class's main objective is to listen when to stop autoscrolling, and
+ * make sure everything stops accordingly.
+ */
+ private class TouchEventHandler implements NativePreviewHandler {
+ @Override
+ public void onPreviewNativeEvent(final NativePreviewEvent event) {
+ /*
+ * Remember: targetElement is always where touchstart started, not
+ * where the finger is pointing currently.
+ */
+ switch (event.getTypeInt()) {
+ case Event.ONTOUCHSTART: {
+ if (event.getNativeEvent().getTouches().length() == 1) {
+ /*
+ * Something has dropped a touchend/touchcancel and the
+ * scroller is most probably running amok. Let's cancel it
+ * and pretend that everything's going as expected
+ *
+ * Because this is a preview, this code is run before start
+ * event can be passed to the start(...) method.
+ */
+ stop();
+
+ /*
+ * Related TODO: investigate why iOS seems to ignore a
+ * touchend/touchcancel when frames are dropped, and/or if
+ * something can be done about that.
+ */
+ }
+ break;
+ }
+
+ case Event.ONTOUCHMOVE:
+ event.cancel();
+ break;
+
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ // TODO investigate if this works as desired
+ stop();
+ break;
+ }
+ }
+
+ }
+
+ /**
+ * This class's responsibility is to scroll the table while a pointer is
+ * kept in a scrolling zone.
+ * <p>
+ * <em>Techical note:</em> This class is an AnimationCallback because we
+ * need a timer: when the finger is kept in place while the grid scrolls, we
+ * still need to be able to make new selections. So, instead of relying on
+ * events (which won't be fired, since the pointer isn't necessarily
+ * moving), we do this check on each frame while the pointer is "active"
+ * (mouse is pressed, finger is on screen).
+ */
+ private class AutoScrollingFrame implements AnimationCallback {
+
+ /**
+ * If the acceleration gradient area is smaller than this, autoscrolling
+ * will be disabled (it becomes too quick to accelerate to be usable).
+ */
+ private static final int GRADIENT_MIN_THRESHOLD_PX = 10;
+
+ /**
+ * The speed at which the gradient area recovers, once scrolling in that
+ * direction has started.
+ */
+ private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1;
+ private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC
+ / 1000.0d;
+
+ /**
+ * The lowest y/x-coordinate on the {@link Event#getClientY() client-y}
+ * or {@link Event#getClientX() client-x} from where we need to start
+ * scrolling towards the top/left.
+ */
+ private int startBound = -1;
+
+ /**
+ * The highest y/x-coordinate on the {@link Event#getClientY() client-y}
+ * or {@link Event#getClientX() client-x} from where we need to
+ * scrolling towards the bottom.
+ */
+ private int endBound = -1;
+
+ /**
+ * The area where the selection acceleration takes place. If &lt;
+ * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled
+ */
+ private final int gradientArea;
+
+ /**
+ * The number of pixels per seconds we currently are scrolling (negative
+ * is towards the top/left, positive is towards the bottom/right).
+ */
+ private double scrollSpeed = 0;
+
+ private double prevTimestamp = 0;
+
+ /**
+ * This field stores fractions of pixels to scroll, to make sure that
+ * we're able to scroll less than one px per frame.
+ */
+ private double pixelsToScroll = 0.0d;
+
+ /** Should this animator be running. */
+ private boolean running = false;
+
+ /** The handle in which this instance is running. */
+ private AnimationHandle handle;
+
+ /**
+ * The pointer's pageY (VERTICAL) / pageX (HORIZONTAL) coordinate
+ * depending on scrolling axis.
+ */
+ private int scrollingAxisPageCoordinate;
+
+ /** @see #doScrollAreaChecks(int) */
+ private int finalStartBound;
+
+ /** @see #doScrollAreaChecks(int) */
+ private int finalEndBound;
+
+ private boolean scrollAreaShouldRebound = false;
+
+ public AutoScrollingFrame(final int startBound, final int endBound,
+ final int gradientArea) {
+ finalStartBound = startBound;
+ finalEndBound = endBound;
+ this.gradientArea = gradientArea;
+ }
+
+ @Override
+ public void execute(final double timestamp) {
+ final double timeDiff = timestamp - prevTimestamp;
+ prevTimestamp = timestamp;
+
+ reboundScrollArea(timeDiff);
+
+ pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d);
+ final int intPixelsToScroll = (int) pixelsToScroll;
+ pixelsToScroll -= intPixelsToScroll;
+
+ if (intPixelsToScroll != 0) {
+ double scrollPos;
+ double maxScrollPos;
+ double newScrollPos;
+ if (scrollDirection == ScrollAxis.VERTICAL) {
+ scrollPos = grid.getScrollTop();
+ maxScrollPos = getMaxScrollTop();
+ } else {
+ scrollPos = grid.getScrollLeft();
+ maxScrollPos = getMaxScrollLeft();
+ }
+ if (intPixelsToScroll > 0 && scrollPos < maxScrollPos
+ || intPixelsToScroll < 0 && scrollPos > 0) {
+ newScrollPos = scrollPos + intPixelsToScroll;
+ if (scrollDirection == ScrollAxis.VERTICAL) {
+ grid.setScrollTop(newScrollPos);
+ } else {
+ grid.setScrollLeft(newScrollPos);
+ }
+ callback.onAutoScroll(intPixelsToScroll);
+ if (newScrollPos <= 0) {
+ callback.onAutoScrollReachedMin();
+ } else if (newScrollPos >= maxScrollPos) {
+ callback.onAutoScrollReachedMax();
+ }
+ }
+ }
+
+ reschedule();
+ }
+
+ /**
+ * If the scroll are has been offset by the pointer starting out there,
+ * move it back a bit
+ */
+ private void reboundScrollArea(double timeDiff) {
+ if (!scrollAreaShouldRebound) {
+ return;
+ }
+
+ int reboundPx = (int) Math
+ .ceil(SCROLL_AREA_REBOUND_PX_PER_MS * timeDiff);
+ if (startBound < finalStartBound) {
+ startBound += reboundPx;
+ startBound = Math.min(startBound, finalStartBound);
+ updateScrollSpeed(scrollingAxisPageCoordinate);
+ } else if (endBound > finalEndBound) {
+ endBound -= reboundPx;
+ endBound = Math.max(endBound, finalEndBound);
+ updateScrollSpeed(scrollingAxisPageCoordinate);
+ }
+ }
+
+ private void updateScrollSpeed(final int pointerPageCordinate) {
+
+ final double ratio;
+ if (pointerPageCordinate < startBound) {
+ final double distance = pointerPageCordinate - startBound;
+ ratio = Math.max(-1, distance / gradientArea);
+ }
+
+ else if (pointerPageCordinate > endBound) {
+ final double distance = pointerPageCordinate - endBound;
+ ratio = Math.min(1, distance / gradientArea);
+ }
+
+ else {
+ ratio = 0;
+ }
+
+ scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
+ }
+
+ public void start() {
+ running = true;
+ reschedule();
+ }
+
+ public void stop() {
+ running = false;
+
+ if (handle != null) {
+ handle.cancel();
+ handle = null;
+ }
+ }
+
+ private void reschedule() {
+ if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) {
+ handle = AnimationScheduler.get().requestAnimationFrame(this,
+ grid.getElement());
+ }
+ }
+
+ public void updatePointerCoords(int pageX, int pageY) {
+ final int pageCordinate;
+ if (scrollDirection == ScrollAxis.VERTICAL) {
+ pageCordinate = pageY;
+ } else {
+ pageCordinate = pageX;
+ }
+ doScrollAreaChecks(pageCordinate);
+ updateScrollSpeed(pageCordinate);
+ scrollingAxisPageCoordinate = pageCordinate;
+ }
+
+ /**
+ * This method checks whether the first pointer event started in an area
+ * that would start scrolling immediately, and does some actions
+ * accordingly.
+ * <p>
+ * If it is, that scroll area will be offset "beyond" the pointer (above
+ * if pointer is towards the top/left, otherwise below/right).
+ */
+ private void doScrollAreaChecks(int pageCordinate) {
+ /*
+ * The first run makes sure that neither scroll position is
+ * underneath the finger, but offset to either direction from
+ * underneath the pointer.
+ */
+ if (startBound == -1) {
+ startBound = Math.min(finalStartBound, pageCordinate);
+ endBound = Math.max(finalEndBound, pageCordinate);
+ }
+
+ /*
+ * Subsequent runs make sure that the scroll area grows (but doesn't
+ * shrink) with the finger, but no further than the final bound.
+ */
+ else {
+ int oldTopBound = startBound;
+ if (startBound < finalStartBound) {
+ startBound = Math.max(startBound,
+ Math.min(finalStartBound, pageCordinate));
+ }
+
+ int oldBottomBound = endBound;
+ if (endBound > finalEndBound) {
+ endBound = Math.min(endBound,
+ Math.max(finalEndBound, pageCordinate));
+ }
+
+ final boolean startDidNotMove = oldTopBound == startBound;
+ final boolean endDidNotMove = oldBottomBound == endBound;
+ final boolean wasMovement = pageCordinate != scrollingAxisPageCoordinate;
+ scrollAreaShouldRebound = (startDidNotMove && endDidNotMove
+ && wasMovement);
+ }
+ }
+ }
+
+ /**
+ * This handler makes sure that pointer movements are handled.
+ * <p>
+ * Essentially, a native preview handler is registered (so that selection
+ * gestures can happen outside of the selection column). The handler itself
+ * makes sure that it's detached when the pointer is "lifted".
+ */
+ private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() {
+ @Override
+ public void onPreviewNativeEvent(final NativePreviewEvent event) {
+ if (autoScroller == null) {
+ stop();
+ return;
+ }
+
+ final NativeEvent nativeEvent = event.getNativeEvent();
+ int pageY = 0;
+ int pageX = 0;
+ switch (event.getTypeInt()) {
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ pageY = WidgetUtil.getTouchOrMouseClientY(nativeEvent);
+ pageX = WidgetUtil.getTouchOrMouseClientX(nativeEvent);
+ autoScroller.updatePointerCoords(pageX, pageY);
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ stop();
+ break;
+ }
+ }
+ };
+ /** The registration info for {@link #scrollPreviewHandler} */
+ private HandlerRegistration handlerRegistration;
+
+ /**
+ * The top/left bound, as calculated from the {@link Event#getClientY()
+ * client-y} or {@link Event#getClientX() client-x} coordinates.
+ */
+ private double startingBound = -1;
+
+ /**
+ * The bottom/right bound, as calculated from the {@link Event#getClientY()
+ * client-y} or or {@link Event#getClientX() client-x} coordinates.
+ */
+ private int endingBound = -1;
+
+ /** The size of the autoscroll acceleration area. */
+ private int gradientArea;
+
+ private Grid<?> grid;
+
+ private HandlerRegistration nativePreviewHandlerRegistration;
+
+ private ScrollAxis scrollDirection;
+
+ private AutoScrollingFrame autoScroller;
+
+ private AutoScrollerCallback callback;
+
+ /**
+ * Creates a new instance for scrolling the given grid.
+ *
+ * @param grid
+ * the grid to auto scroll
+ */
+ public AutoScroller(Grid<?> grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Starts the automatic scrolling detection.
+ *
+ * @param startEvent
+ * the event that starts the automatic scroll
+ * @param scrollAxis
+ * the axis along which the scrolling should happen
+ * @param callback
+ * the callback for getting info about the automatic scrolling
+ */
+ public void start(final NativeEvent startEvent, ScrollAxis scrollAxis,
+ AutoScrollerCallback callback) {
+ scrollDirection = scrollAxis;
+ this.callback = callback;
+ injectNativeHandler();
+ start();
+ startEvent.preventDefault();
+ startEvent.stopPropagation();
+ }
+
+ /**
+ * Stops the automatic scrolling.
+ */
+ public void stop() {
+ if (handlerRegistration != null) {
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ }
+
+ if (autoScroller != null) {
+ autoScroller.stop();
+ autoScroller = null;
+ }
+
+ removeNativeHandler();
+ }
+
+ /**
+ * Set the auto scroll area height or width depending on the scrolling axis.
+ * This is the amount of pixels from the edge of the grid that the scroll is
+ * triggered.
+ * <p>
+ * Defaults to 100px.
+ *
+ * @param px
+ * the pixel height/width for the auto scroll area depending on
+ * direction
+ */
+ public void setScrollArea(int px) {
+ scrollAreaPX = px;
+ }
+
+ /**
+ * Returns the size of the auto scroll area in pixels.
+ * <p>
+ * Defaults to 100px.
+ *
+ * @return size in pixels
+ */
+ public int getScrollArea() {
+ return scrollAreaPX;
+ }
+
+ private void start() {
+ /*
+ * bounds are updated whenever the autoscroll cycle starts, to make sure
+ * that the widget hasn't changed in size, moved around, or whatnot.
+ */
+ updateScrollBounds();
+
+ assert handlerRegistration == null : "handlerRegistration was not null";
+ assert autoScroller == null : "autoScroller was not null";
+ handlerRegistration = Event
+ .addNativePreviewHandler(scrollPreviewHandler);
+ autoScroller = new AutoScrollingFrame((int) Math.ceil(startingBound),
+ endingBound, gradientArea);
+ autoScroller.start();
+ }
+
+ private void updateScrollBounds() {
+ double startBorder = getBodyClientStart();
+ final int endBorder = getBodyClientEnd();
+ startBorder += getFrozenColumnsWidth();
+
+ startingBound = startBorder + scrollAreaPX;
+ endingBound = endBorder - scrollAreaPX;
+ gradientArea = scrollAreaPX;
+
+ // modify bounds if they're too tightly packed
+ if (endingBound - startingBound < MIN_NO_AUTOSCROLL_AREA_PX) {
+ double adjustment = MIN_NO_AUTOSCROLL_AREA_PX
+ - (endingBound - startingBound);
+ startingBound -= adjustment / 2;
+ endingBound += adjustment / 2;
+ gradientArea -= adjustment / 2;
+ }
+ }
+
+ private void injectNativeHandler() {
+ removeNativeHandler();
+ nativePreviewHandlerRegistration = Event
+ .addNativePreviewHandler(new TouchEventHandler());
+ }
+
+ private void removeNativeHandler() {
+ if (nativePreviewHandlerRegistration != null) {
+ nativePreviewHandlerRegistration.removeHandler();
+ nativePreviewHandlerRegistration = null;
+ }
+ }
+
+ private TableElement getTableElement() {
+ final Element root = grid.getElement();
+ final Element tablewrapper = Element.as(root.getChild(2));
+ if (tablewrapper != null) {
+ return TableElement.as(tablewrapper.getFirstChildElement());
+ } else {
+ return null;
+ }
+ }
+
+ private TableSectionElement getTheadElement() {
+ TableElement table = getTableElement();
+ if (table != null) {
+ return table.getTHead();
+ } else {
+ return null;
+ }
+ }
+
+ private TableSectionElement getTfootElement() {
+ TableElement table = getTableElement();
+ if (table != null) {
+ return table.getTFoot();
+ } else {
+ return null;
+ }
+ }
+
+ private int getBodyClientEnd() {
+ if (scrollDirection == ScrollAxis.VERTICAL) {
+ return getTfootElement().getAbsoluteTop() - 1;
+ } else {
+ return getTableElement().getAbsoluteRight();
+ }
+
+ }
+
+ private int getBodyClientStart() {
+ if (scrollDirection == ScrollAxis.VERTICAL) {
+ return getTheadElement().getAbsoluteBottom() + 1;
+ } else {
+ return getTableElement().getAbsoluteLeft();
+ }
+ }
+
+ public double getFrozenColumnsWidth() {
+ double value = 0;
+
+ for (int i = 0; i < getRealFrozenColumnCount(); i++) {
+ value += grid.getColumn(i).getWidthActual();
+ }
+
+ return value;
+ }
+
+ private int getRealFrozenColumnCount() {
+ if (grid.getFrozenColumnCount() < 0) {
+ return 0;
+ } else if (grid.getSelectionModel()
+ .getSelectionColumnRenderer() != null) {
+ // includes the selection column
+ return grid.getFrozenColumnCount() + 1;
+ } else {
+ return grid.getFrozenColumnCount();
+ }
+ }
+
+ private double getMaxScrollLeft() {
+ return grid.getScrollWidth()
+ - (getTableElement().getParentElement().getOffsetWidth()
+ - getFrozenColumnsWidth());
+ }
+
+ private double getMaxScrollTop() {
+ return grid.getScrollHeight() - getTfootElement().getOffsetHeight()
+ - getTheadElement().getOffsetHeight();
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/CellReference.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/CellReference.java
new file mode 100644
index 0000000000..55c19c9bff
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/CellReference.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * 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.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the type of the row object containing this cell
+ * @since 7.4
+ */
+public class CellReference<T> {
+
+ private int columnIndexDOM;
+ private int columnIndex;
+ private Grid.Column<?, T> column;
+ private final RowReference<T> rowReference;
+
+ public CellReference(RowReference<T> rowReference) {
+ this.rowReference = rowReference;
+ }
+
+ /**
+ * Sets the identifying information for this cell.
+ * <p>
+ * The difference between {@link #columnIndexDOM} and {@link #columnIndex}
+ * comes from hidden columns.
+ *
+ * @param columnIndexDOM
+ * the index of the column in the DOM
+ * @param columnIndex
+ * the index of the column
+ * @param column
+ * the column object
+ */
+ public void set(int columnIndexDOM, int columnIndex,
+ Grid.Column<?, T> column) {
+ this.columnIndexDOM = columnIndexDOM;
+ this.columnIndex = columnIndex;
+ this.column = column;
+ }
+
+ /**
+ * Gets the grid that contains the referenced cell.
+ *
+ * @return the grid that contains referenced cell
+ */
+ public Grid<T> getGrid() {
+ return rowReference.getGrid();
+ }
+
+ /**
+ * Gets the row index of the row.
+ *
+ * @return the index of the row
+ */
+ public int getRowIndex() {
+ return rowReference.getRowIndex();
+ }
+
+ /**
+ * Gets the row data object.
+ *
+ * @return the row object
+ */
+ public T getRow() {
+ return rowReference.getRow();
+ }
+
+ /**
+ * Gets the index of the column.
+ * <p>
+ * <em>NOTE:</em> The index includes hidden columns in the count, unlike
+ * {@link #getColumnIndexDOM()}.
+ *
+ * @return the index of the column
+ */
+ public int getColumnIndex() {
+ return columnIndex;
+ }
+
+ /**
+ * Gets the index of the cell in the DOM. The difference to
+ * {@link #getColumnIndex()} is caused by hidden columns.
+ *
+ * @since 7.5.0
+ * @return the index of the column in the DOM
+ */
+ public int getColumnIndexDOM() {
+ return columnIndexDOM;
+ }
+
+ /**
+ * Gets the column objects.
+ *
+ * @return the column object
+ */
+ public Grid.Column<?, T> getColumn() {
+ return column;
+ }
+
+ /**
+ * Gets the value of the cell.
+ *
+ * @return the value of the cell
+ */
+ public Object getValue() {
+ return getColumn().getValue(getRow());
+ }
+
+ /**
+ * Get the element of the cell.
+ *
+ * @return the element of the cell
+ */
+ public TableCellElement getElement() {
+ return rowReference.getElement().getCells().getItem(columnIndexDOM);
+ }
+
+ /**
+ * Gets the RowReference for this CellReference.
+ *
+ * @return the row reference
+ */
+ protected RowReference<T> getRowReference() {
+ return rowReference;
+ }
+
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/CellStyleGenerator.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/CellStyleGenerator.java
new file mode 100644
index 0000000000..30e4545854
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/CellStyleGenerator.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Callback interface for generating custom style names for cells
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type of the target grid
+ * @see Grid#setCellStyleGenerator(CellStyleGenerator)
+ * @since 7.4
+ */
+public interface CellStyleGenerator<T> {
+
+ /**
+ * Called by Grid to generate a style name for a column element.
+ *
+ * @param cellReference
+ * 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 abstract String getStyle(CellReference<T> cellReference);
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableEvent.java
new file mode 100644
index 0000000000..37b171894d
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.shared.ui.grid.Range;
+
+/**
+ * Event object describing a change of row availability in DataSource of a Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DataAvailableEvent extends GwtEvent<DataAvailableHandler> {
+
+ private Range rowsAvailable;
+ public static final Type<DataAvailableHandler> TYPE = new Type<DataAvailableHandler>();
+
+ public DataAvailableEvent(Range rowsAvailable) {
+ this.rowsAvailable = rowsAvailable;
+ }
+
+ /**
+ * Returns the range of available rows in {@link DataSource} for this event.
+ *
+ * @return range of available rows
+ */
+ public Range getAvailableRows() {
+ return rowsAvailable;
+ }
+
+ @Override
+ public Type<DataAvailableHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(DataAvailableHandler handler) {
+ handler.onDataAvailable(this);
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableHandler.java
new file mode 100644
index 0000000000..ee8bdbdd79
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for {@link DataAvailableEvent}s.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface DataAvailableHandler extends EventHandler {
+
+ /**
+ * Called when DataSource has data available. Supplied with row range.
+ *
+ * @param availableRows
+ * Range of rows available in the DataSource
+ * @return true if the command was successfully completed, false to call
+ * again the next time new data is available
+ */
+ public void onDataAvailable(DataAvailableEvent event);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DefaultEditorEventHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DefaultEditorEventHandler.java
new file mode 100644
index 0000000000..9f4eace77c
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DefaultEditorEventHandler.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.FocusUtil;
+import com.vaadin.v7.client.widgets.Grid.Editor;
+import com.vaadin.v7.client.widgets.Grid.EditorDomEvent;
+
+/**
+ * The default handler for Grid editor events. Offers several overridable
+ * protected methods for easier customization.
+ *
+ * @since 7.6
+ * @author Vaadin Ltd
+ */
+public class DefaultEditorEventHandler<T> implements Editor.EventHandler<T> {
+
+ public static final int KEYCODE_OPEN = KeyCodes.KEY_ENTER;
+ public static final int KEYCODE_MOVE_VERTICAL = KeyCodes.KEY_ENTER;
+ public static final int KEYCODE_CLOSE = KeyCodes.KEY_ESCAPE;
+ public static final int KEYCODE_MOVE_HORIZONTAL = KeyCodes.KEY_TAB;
+ public static final int KEYCODE_BUFFERED_SAVE = KeyCodes.KEY_ENTER;
+
+ private double lastTouchEventTime = 0;
+ private int lastTouchEventX = -1;
+ private int lastTouchEventY = -1;
+ private int lastTouchEventRow = -1;
+
+ /**
+ * Returns whether the given event is a touch event that should open the
+ * editor.
+ *
+ * @param event
+ * the received event
+ * @return whether the event is a touch open event
+ */
+ protected boolean isTouchOpenEvent(EditorDomEvent<T> event) {
+ final Event e = event.getDomEvent();
+ final int type = e.getTypeInt();
+
+ final double now = Duration.currentTimeMillis();
+ final int currentX = WidgetUtil.getTouchOrMouseClientX(e);
+ final int currentY = WidgetUtil.getTouchOrMouseClientY(e);
+
+ final boolean validTouchOpenEvent = type == Event.ONTOUCHEND
+ && now - lastTouchEventTime < 500
+ && lastTouchEventRow == event.getCell().getRowIndex()
+ && Math.abs(lastTouchEventX - currentX) < 20
+ && Math.abs(lastTouchEventY - currentY) < 20;
+
+ if (type == Event.ONTOUCHSTART) {
+ lastTouchEventX = currentX;
+ lastTouchEventY = currentY;
+ }
+
+ if (type == Event.ONTOUCHEND) {
+ lastTouchEventTime = now;
+ lastTouchEventRow = event.getCell().getRowIndex();
+ }
+
+ return validTouchOpenEvent;
+ }
+
+ /**
+ * Returns whether the given event should open the editor. The default
+ * implementation returns true if and only if the event is a doubleclick or
+ * if it is a keydown event and the keycode is {@link #KEYCODE_OPEN}.
+ *
+ * @param event
+ * the received event
+ * @return true if the event is an open event, false otherwise
+ */
+ protected boolean isOpenEvent(EditorDomEvent<T> event) {
+ final Event e = event.getDomEvent();
+ return e.getTypeInt() == Event.ONDBLCLICK
+ || (e.getTypeInt() == Event.ONKEYDOWN
+ && e.getKeyCode() == KEYCODE_OPEN)
+ || isTouchOpenEvent(event);
+ }
+
+ /**
+ * Opens the editor on the appropriate row if the received event is an open
+ * event. The default implementation uses
+ * {@link #isOpenEvent(EditorDomEvent) isOpenEvent}.
+ *
+ * @param event
+ * the received event
+ * @return true if this method handled the event and nothing else should be
+ * done, false otherwise
+ */
+ protected boolean handleOpenEvent(EditorDomEvent<T> event) {
+ if (isOpenEvent(event)) {
+ final EventCellReference<T> cell = event.getCell();
+
+ editRow(event, cell.getRowIndex(), cell.getColumnIndexDOM());
+
+ event.getDomEvent().preventDefault();
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Moves the editor to another row or another column if the received event
+ * is a move event. The default implementation moves the editor to the
+ * clicked row if the event is a click; otherwise, if the event is a keydown
+ * and the keycode is {@link #KEYCODE_MOVE_VERTICAL}, moves the editor one
+ * row up or down if the shift key is pressed or not, respectively. Keydown
+ * event with keycode {@link #KEYCODE_MOVE_HORIZONTAL} moves the editor left
+ * or right if shift key is pressed or not, respectively.
+ *
+ * @param event
+ * the received event
+ * @return true if this method handled the event and nothing else should be
+ * done, false otherwise
+ */
+ protected boolean handleMoveEvent(EditorDomEvent<T> event) {
+ Event e = event.getDomEvent();
+ final EventCellReference<T> cell = event.getCell();
+
+ // TODO: Move on touch events
+ if (e.getTypeInt() == Event.ONCLICK) {
+
+ editRow(event, cell.getRowIndex(), cell.getColumnIndexDOM());
+
+ return true;
+ }
+
+ else if (e.getTypeInt() == Event.ONKEYDOWN) {
+
+ int rowDelta = 0;
+ int colDelta = 0;
+
+ if (e.getKeyCode() == KEYCODE_MOVE_VERTICAL) {
+ rowDelta = (e.getShiftKey() ? -1 : +1);
+ } else if (e.getKeyCode() == KEYCODE_MOVE_HORIZONTAL) {
+ colDelta = (e.getShiftKey() ? -1 : +1);
+ // Prevent tab out of Grid Editor
+ event.getDomEvent().preventDefault();
+ }
+
+ final boolean changed = rowDelta != 0 || colDelta != 0;
+
+ if (changed) {
+
+ int columnCount = event.getGrid().getVisibleColumns().size();
+
+ int colIndex = event.getFocusedColumnIndex() + colDelta;
+ int rowIndex = event.getRowIndex();
+
+ // Handle row change with horizontal move when column goes out
+ // of range.
+ if (rowDelta == 0) {
+ if (colIndex >= columnCount
+ && rowIndex < event.getGrid().getDataSource().size()
+ - 1) {
+ rowDelta = 1;
+ colIndex = 0;
+ } else if (colIndex < 0 && rowIndex > 0) {
+ rowDelta = -1;
+ colIndex = columnCount - 1;
+ }
+ }
+
+ editRow(event, rowIndex + rowDelta, colIndex);
+ }
+
+ return changed;
+ }
+
+ return false;
+ }
+
+ /**
+ * Moves the editor to another column if the received event is a move event.
+ * By default the editor is moved on a keydown event with keycode
+ * {@link #KEYCODE_MOVE_HORIZONTAL}. This moves the editor left or right if
+ * shift key is pressed or not, respectively.
+ *
+ * @param event
+ * the received event
+ * @return true if this method handled the event and nothing else should be
+ * done, false otherwise
+ */
+ protected boolean handleBufferedMoveEvent(EditorDomEvent<T> event) {
+ Event e = event.getDomEvent();
+
+ if (e.getType().equals(BrowserEvents.CLICK)
+ && event.getRowIndex() == event.getCell().getRowIndex()) {
+
+ editRow(event, event.getRowIndex(),
+ event.getCell().getColumnIndexDOM());
+
+ return true;
+
+ } else if (e.getType().equals(BrowserEvents.KEYDOWN)
+ && e.getKeyCode() == KEYCODE_MOVE_HORIZONTAL) {
+
+ // Prevent tab out of Grid Editor
+ event.getDomEvent().preventDefault();
+
+ editRow(event, event.getRowIndex(), event.getFocusedColumnIndex()
+ + (e.getShiftKey() ? -1 : +1));
+
+ return true;
+ } else if (e.getType().equals(BrowserEvents.KEYDOWN)
+ && e.getKeyCode() == KEYCODE_BUFFERED_SAVE) {
+ triggerValueChangeEvent(event);
+
+ // Save and close.
+ event.getGrid().getEditor().save();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns whether the given event should close the editor. The default
+ * implementation returns true if and only if the event is a keydown event
+ * and the keycode is {@link #KEYCODE_CLOSE}.
+ *
+ * @param event
+ * the received event
+ * @return true if the event is a close event, false otherwise
+ */
+ protected boolean isCloseEvent(EditorDomEvent<T> event) {
+ final Event e = event.getDomEvent();
+ return e.getTypeInt() == Event.ONKEYDOWN
+ && e.getKeyCode() == KEYCODE_CLOSE;
+ }
+
+ /**
+ * Closes the editor if the received event is a close event. The default
+ * implementation uses {@link #isCloseEvent(EditorDomEvent) isCloseEvent}.
+ *
+ * @param event
+ * the received event
+ * @return true if this method handled the event and nothing else should be
+ * done, false otherwise
+ */
+ protected boolean handleCloseEvent(EditorDomEvent<T> event) {
+ if (isCloseEvent(event)) {
+ event.getEditor().cancel();
+ FocusUtil.setFocus(event.getGrid(), true);
+ return true;
+ }
+ return false;
+ }
+
+ protected void editRow(EditorDomEvent<T> event, int rowIndex,
+ int colIndex) {
+ int rowCount = event.getGrid().getDataSource().size();
+ // Limit rowIndex between 0 and rowCount - 1
+ rowIndex = Math.max(0, Math.min(rowCount - 1, rowIndex));
+
+ int colCount = event.getGrid().getVisibleColumns().size();
+ // Limit colIndex between 0 and colCount - 1
+ colIndex = Math.max(0, Math.min(colCount - 1, colIndex));
+
+ if (rowIndex != event.getRowIndex()) {
+ triggerValueChangeEvent(event);
+ }
+
+ event.getEditor().editRow(rowIndex, colIndex);
+ }
+
+ /**
+ * Triggers a value change event from the editor field if it has focus. This
+ * is based on the assumption that editor field will fire the value change
+ * when a blur event occurs.
+ *
+ * @param event
+ * the editor DOM event
+ */
+ private void triggerValueChangeEvent(EditorDomEvent<T> event) {
+ // Force a blur to cause a value change event
+ Widget editorWidget = event.getEditorWidget();
+ if (editorWidget != null) {
+ Element focusedElement = WidgetUtil.getFocusedElement();
+ if (editorWidget.getElement().isOrHasChild(focusedElement)) {
+ focusedElement.blur();
+ focusedElement.focus();
+ }
+ }
+ }
+
+ @Override
+ public boolean handleEvent(EditorDomEvent<T> event) {
+ final Editor<T> editor = event.getEditor();
+ final boolean isBody = event.getCell().isBody();
+
+ final boolean handled;
+ if (event.getGrid().isEditorActive()) {
+ handled = handleCloseEvent(event)
+ || (!editor.isBuffered() && isBody
+ && handleMoveEvent(event))
+ || (editor.isBuffered() && isBody
+ && handleBufferedMoveEvent(event));
+ } else {
+ handled = event.getGrid().isEnabled() && isBody
+ && handleOpenEvent(event);
+ }
+
+ // Buffered mode should swallow all events, if not already handled.
+ boolean swallowEvent = event.getGrid().isEditorActive()
+ && editor.isBuffered();
+
+ return handled || swallowEvent;
+ }
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DetailsGenerator.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DetailsGenerator.java
new file mode 100644
index 0000000000..04c8ff42cd
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DetailsGenerator.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * A callback interface for generating details for a particular row in Grid.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ */
+public interface DetailsGenerator {
+
+ /** A details generator that provides no details */
+ public static final DetailsGenerator NULL = new DetailsGenerator() {
+ @Override
+ public Widget getDetails(int rowIndex) {
+ return null;
+ }
+ };
+
+ /**
+ * This method is called for whenever a new details row needs to be
+ * generated.
+ *
+ * @param rowIndex
+ * the index of the row for which to generate details
+ * @return the details for the given row, or <code>null</code> to leave the
+ * details empty.
+ */
+ Widget getDetails(int rowIndex);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/EditorHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/EditorHandler.java
new file mode 100644
index 0000000000..a0fee4c7fc
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/EditorHandler.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import java.util.Collection;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * An interface for binding widgets and data to the grid row editor. Used by the
+ * editor to support different row types, data sources and custom data binding
+ * mechanisms.
+ *
+ * @param <T>
+ * the row data type
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface EditorHandler<T> {
+
+ /**
+ * A request class passed as a parameter to the editor handler methods. The
+ * request is callback-based to facilitate usage with remote or otherwise
+ * asynchronous data sources.
+ * <p>
+ * An implementation must call either {@link #success()} or {@link #fail()},
+ * according to whether the operation was a success or failed during
+ * execution, respectively.
+ *
+ * @param <T>
+ * the row data type
+ */
+ public interface EditorRequest<T> {
+ /**
+ * Returns the index of the row being requested.
+ *
+ * @return the row index
+ */
+ public int getRowIndex();
+
+ /**
+ * Returns the index of the column being focused.
+ *
+ * @return the column index
+ */
+ public int getColumnIndex();
+
+ /**
+ * Returns the row data related to the row being requested.
+ *
+ * @return the row data
+ */
+ public T getRow();
+
+ /**
+ * Returns the grid instance related to this editor request.
+ *
+ * @return the grid instance
+ */
+ public Grid<T> getGrid();
+
+ /**
+ * Returns the editor widget used to edit the values of the given
+ * column.
+ *
+ * @param column
+ * the column whose widget to get
+ * @return the widget related to the column
+ */
+ public Widget getWidget(Grid.Column<?, T> column);
+
+ /**
+ * Informs Grid that the editor request was a success.
+ */
+ public void success();
+
+ /**
+ * Informs Grid that an error occurred while trying to process the
+ * request.
+ *
+ * @param errorMessage
+ * and error message to show to the user, or
+ * <code>null</code> to not show any message.
+ * @param errorColumns
+ * a collection of columns for which an error indicator
+ * should be shown, or <code>null</code> if no columns should
+ * be marked as erroneous.
+ */
+ public void failure(String errorMessage,
+ Collection<Grid.Column<?, T>> errorColumns);
+
+ /**
+ * Checks whether the request is completed or not.
+ *
+ * @return <code>true</code> iff the request is completed
+ */
+ public boolean isCompleted();
+ }
+
+ /**
+ * Binds row data to the editor widgets. Called by the editor when it is
+ * opened for editing.
+ * <p>
+ * The implementation <em>must</em> call either
+ * {@link EditorRequest#success()} or
+ * {@link EditorRequest#failure(String, Collection)} to signal a successful
+ * or a failed (respectively) bind action.
+ *
+ * @param request
+ * the data binding request
+ *
+ * @see Grid#editRow(int)
+ */
+ public void bind(EditorRequest<T> request);
+
+ /**
+ * Called by the editor when editing is cancelled. This method may have an
+ * empty implementation in case no special processing is required.
+ * <p>
+ * In contrast to {@link #bind(EditorRequest)} and
+ * {@link #save(EditorRequest)}, any calls to
+ * {@link EditorRequest#success()} or
+ * {@link EditorRequest#failure(String, Collection)} have no effect on the
+ * outcome of the cancel action. The editor is already closed when this
+ * method is called.
+ *
+ * @param request
+ * the cancel request
+ *
+ * @see Grid#cancelEditor()
+ */
+ public void cancel(EditorRequest<T> request);
+
+ /**
+ * Commits changes in the currently active edit to the data source. Called
+ * by the editor when changes are saved.
+ * <p>
+ * The implementation <em>must</em> call either
+ * {@link EditorRequest#success()} or {@link EditorRequest#fail()} to signal
+ * a successful or a failed (respectively) save action.
+ *
+ * @param request
+ * the save request
+ *
+ * @see Grid#saveEditor()
+ */
+ public void save(EditorRequest<T> request);
+
+ /**
+ * Returns a widget instance that is used to edit the values in the given
+ * column. A null return value means the column is not editable.
+ *
+ * @param column
+ * the column whose values should be edited
+ * @return the editor widget for the column or null if the column is not
+ * editable
+ */
+ public Widget getWidget(Grid.Column<?, T> column);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/EventCellReference.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/EventCellReference.java
new file mode 100644
index 0000000000..b4fc74c6e1
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/EventCellReference.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.v7.client.widget.escalator.Cell;
+import com.vaadin.v7.client.widgets.Grid;
+import com.vaadin.v7.client.widgets.Grid.Column;
+
+/**
+ * A data class which contains information which identifies a cell being the
+ * target of an event from {@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.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class EventCellReference<T> extends CellReference<T> {
+
+ private Section section;
+ private TableCellElement element;
+
+ public EventCellReference(Grid<T> grid) {
+ super(new RowReference<T>(grid));
+ }
+
+ /**
+ * Sets the RowReference and CellReference to point to given Cell.
+ *
+ * @param targetCell
+ * cell to point to
+ */
+ public void set(Cell targetCell, Section section) {
+ Grid<T> grid = getGrid();
+
+ int columnIndexDOM = targetCell.getColumn();
+ Column<?, T> column = null;
+ if (columnIndexDOM >= 0
+ && columnIndexDOM < grid.getVisibleColumns().size()) {
+ column = grid.getVisibleColumns().get(columnIndexDOM);
+ }
+
+ int row = targetCell.getRow();
+ // Row objects only make sense for body section of Grid.
+ T rowObject;
+ if (section == Section.BODY && row >= 0
+ && row < grid.getDataSource().size()) {
+ rowObject = grid.getDataSource().getRow(row);
+ } else {
+ rowObject = null;
+ }
+
+ // At least for now we don't need to have the actual TableRowElement
+ // available.
+ getRowReference().set(row, rowObject, null);
+
+ int columnIndex = grid.getColumns().indexOf(column);
+ set(columnIndexDOM, columnIndex, column);
+
+ this.element = targetCell.getElement();
+ this.section = section;
+ }
+
+ @Override
+ public TableCellElement getElement() {
+ return element;
+ }
+
+ /**
+ * Is the cell reference for a cell in the header of the Grid.
+ *
+ * @since 7.5
+ * @return <code>true</true> if referenced cell is in the header,
+ * <code>false</code> if not
+ */
+ public boolean isHeader() {
+ return section == Section.HEADER;
+ }
+
+ /**
+ * Is the cell reference for a cell in the body of the Grid.
+ *
+ * @since 7.5
+ * @return <code>true</true> if referenced cell is in the body,
+ * <code>false</code> if not
+ */
+ public boolean isBody() {
+ return section == Section.BODY;
+ }
+
+ /**
+ * Is the cell reference for a cell in the footer of the Grid.
+ *
+ * @since 7.5
+ * @return <code>true</true> if referenced cell is in the footer,
+ * <code>false</code> if not
+ */
+ public boolean isFooter() {
+ return section == Section.FOOTER;
+ }
+
+ /**
+ * Gets the Grid section where the referenced cell is.
+ *
+ * @since 7.5
+ * @return grid section
+ */
+ public Section getSection() {
+ return section;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/HeightAwareDetailsGenerator.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/HeightAwareDetailsGenerator.java
new file mode 100644
index 0000000000..73ebd33dbd
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/HeightAwareDetailsGenerator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+/**
+ * {@link DetailsGenerator} that is aware of content heights.
+ * <p>
+ * <b>FOR INTERNAL USE ONLY!</b> This class exists only for the sake of a
+ * temporary workaround and might be removed or renamed at any time.
+ * </p>
+ *
+ * @since 7.6.1
+ * @author Vaadin Ltd
+ */
+@Deprecated
+public interface HeightAwareDetailsGenerator extends DetailsGenerator {
+
+ /**
+ * This method is called for whenever a details row's height needs to be
+ * calculated.
+ * <p>
+ * <b>FOR INTERNAL USE ONLY!</b> This method exists only for the sake of a
+ * temporary workaround and might be removed or renamed at any time.
+ * </p>
+ *
+ * @since 7.6.1
+ * @param rowIndex
+ * the index of the row for which to calculate details row height
+ * @return height of the details row
+ */
+ public double getDetailsHeight(int rowIndex);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RendererCellReference.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RendererCellReference.java
new file mode 100644
index 0000000000..0431e9442b
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RendererCellReference.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.v7.client.widget.escalator.FlyweightCell;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * A data class which contains information which identifies a cell being
+ * rendered 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.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class RendererCellReference extends CellReference<Object> {
+
+ /**
+ * Creates a new renderer cell reference bound to a row reference.
+ *
+ * @param rowReference
+ * the row reference to bind to
+ */
+ public RendererCellReference(RowReference<Object> rowReference) {
+ super(rowReference);
+ }
+
+ private FlyweightCell cell;
+
+ /**
+ * Sets the identifying information for this cell.
+ *
+ * @param cell
+ * the flyweight cell to reference
+ * @param columnIndex
+ * the index of the column in the grid, including hidden cells
+ * @param column
+ * the column to reference
+ */
+ public void set(FlyweightCell cell, int columnIndex,
+ Grid.Column<?, ?> column) {
+ this.cell = cell;
+ super.set(cell.getColumn(), columnIndex,
+ (Grid.Column<?, Object>) column);
+ }
+
+ /**
+ * Returns the element of the cell. Can be either a <code>TD</code> element
+ * or a <code>TH</code> element.
+ *
+ * @return the element of the cell
+ */
+ @Override
+ public TableCellElement getElement() {
+ return cell.getElement();
+ }
+
+ /**
+ * Sets the colspan attribute of the element of this cell.
+ *
+ * @param numberOfCells
+ * the number of columns that the cell should span
+ */
+ public void setColSpan(int numberOfCells) {
+ cell.setColSpan(numberOfCells);
+ }
+
+ /**
+ * Gets the colspan attribute of the element of this cell.
+ *
+ * @return the number of columns that the cell should span
+ */
+ public int getColSpan() {
+ return cell.getColSpan();
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RowReference.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RowReference.java
new file mode 100644
index 0000000000..7df5e5843f
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RowReference.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import com.google.gwt.dom.client.TableRowElement;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * 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.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row object type
+ * @since 7.4
+ */
+public class RowReference<T> {
+ private final Grid<T> grid;
+
+ private int rowIndex;
+ private T row;
+
+ private TableRowElement element;
+
+ /**
+ * Creates a new row reference for the given grid.
+ *
+ * @param grid
+ * the grid that the row belongs to
+ */
+ public RowReference(Grid<T> grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the identifying information for this row.
+ *
+ * @param rowIndex
+ * the index of the row
+ * @param row
+ * the row object
+ * @param elemenet
+ * the element of the row
+ */
+ public void set(int rowIndex, T row, TableRowElement element) {
+ this.rowIndex = rowIndex;
+ this.row = row;
+ this.element = element;
+ }
+
+ /**
+ * Gets the grid that contains the referenced row.
+ *
+ * @return the grid that contains referenced row
+ */
+ public Grid<T> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the row index of the row.
+ *
+ * @return the index of the row
+ */
+ public int getRowIndex() {
+ return rowIndex;
+ }
+
+ /**
+ * Gets the row data object.
+ *
+ * @return the row object
+ */
+ public T getRow() {
+ return row;
+ }
+
+ /**
+ * Gets the table row element of the row.
+ *
+ * @return the element of the row
+ */
+ public TableRowElement getElement() {
+ return element;
+ }
+
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RowStyleGenerator.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RowStyleGenerator.java
new file mode 100644
index 0000000000..194041e894
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/RowStyleGenerator.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid;
+
+import java.io.Serializable;
+
+/**
+ * Callback interface for generating custom style names for data rows
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type of the target grid
+ * @see Grid#setRowStyleGenerator(RowStyleGenerator)
+ * @since 7.4
+ */
+public interface RowStyleGenerator<T> extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a row.
+ *
+ * @param rowReference
+ * 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 abstract String getStyle(RowReference<T> rowReference);
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/datasources/ListDataSource.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/datasources/ListDataSource.java
new file mode 100644
index 0000000000..994e8059eb
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/datasources/ListDataSource.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.datasources;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.shared.Registration;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.v7.client.widget.grid.events.SelectAllEvent;
+import com.vaadin.v7.client.widget.grid.events.SelectAllHandler;
+
+/**
+ * A simple list based on an in-memory data source for simply adding a list of
+ * row pojos to the grid. Based on a wrapped list instance which supports adding
+ * and removing of items.
+ *
+ * <p>
+ * Usage:
+ *
+ * <pre>
+ * ListDataSource&lt;Integer&gt; ds = new ListDataSource&lt;Integer&gt;(1, 2, 3, 4);
+ *
+ * // Add item to the data source
+ * ds.asList().add(5);
+ *
+ * // Remove item from the data source
+ * ds.asList().remove(3);
+ *
+ * // Add multiple items
+ * ds.asList().addAll(Arrays.asList(5, 6, 7));
+ * </pre>
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ListDataSource<T> implements DataSource<T> {
+
+ private class RowHandleImpl extends RowHandle<T> {
+
+ private final T row;
+
+ public RowHandleImpl(T row) {
+ this.row = row;
+ }
+
+ @Override
+ public T getRow() {
+ /*
+ * We'll cheat here and don't throw an IllegalStateException even if
+ * this isn't pinned, because we know that the reference never gets
+ * stale.
+ */
+ return row;
+ }
+
+ @Override
+ public void pin() {
+ // NOOP, really
+ }
+
+ @Override
+ public void unpin() throws IllegalStateException {
+ /*
+ * Just to make things easier for everyone, we won't throw the
+ * exception, even in illegal situations.
+ */
+ }
+
+ @Override
+ protected boolean equalsExplicit(Object obj) {
+ if (obj instanceof ListDataSource.RowHandleImpl) {
+ /*
+ * Java prefers AbstractRemoteDataSource<?>.RowHandleImpl. I
+ * like the @SuppressWarnings more (keeps the line length in
+ * check.)
+ */
+ @SuppressWarnings("unchecked")
+ RowHandleImpl rhi = (RowHandleImpl) obj;
+ return SharedUtil.equals(row, rhi.row);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected int hashCodeExplicit() {
+ return row.hashCode();
+ }
+
+ @Override
+ public void updateRow() {
+ getHandlers()
+ .forEach(dch -> dch.dataUpdated(ds.indexOf(getRow()), 1));
+ }
+ }
+
+ /**
+ * Wraps the datasource list and notifies the change handler of changing to
+ * the list
+ */
+ private class ListWrapper implements List<T> {
+
+ @Override
+ public int size() {
+ return ds.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return ds.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return ds.contains(o);
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return new ListWrapperIterator(ds.iterator());
+ }
+
+ @Override
+ public Object[] toArray() {
+ return ds.toArray();
+ }
+
+ @Override
+ @SuppressWarnings("hiding")
+ public <T> T[] toArray(T[] a) {
+ return ds.toArray(a);
+ }
+
+ @Override
+ public boolean add(T e) {
+ if (ds.add(e)) {
+ getHandlers().forEach(dch -> dch.dataAdded(ds.size() - 1, 1));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ int index = ds.indexOf(o);
+ if (ds.remove(o)) {
+ getHandlers().forEach(dch -> dch.dataRemoved(index, 1));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return ds.containsAll(c);
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends T> c) {
+ int idx = ds.size();
+ if (ds.addAll(c)) {
+ getHandlers().forEach(dch -> dch.dataAdded(idx, c.size()));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection<? extends T> c) {
+ if (ds.addAll(index, c)) {
+ getHandlers().forEach(dch -> dch.dataAdded(index, c.size()));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ if (ds.removeAll(c)) {
+ getHandlers().forEach(dch -> dch.dataUpdated(0, ds.size()));
+ getHandlers().forEach(dch -> dch.dataAvailable(0, ds.size()));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ if (ds.retainAll(c)) {
+ getHandlers().forEach(dch -> dch.dataUpdated(0, ds.size()));
+ getHandlers().forEach(dch -> dch.dataAvailable(0, ds.size()));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ int size = ds.size();
+ ds.clear();
+ getHandlers().forEach(dch -> dch.dataRemoved(0, size));
+ }
+
+ @Override
+ public T get(int index) {
+ return ds.get(index);
+ }
+
+ @Override
+ public T set(int index, T element) {
+ T prev = ds.set(index, element);
+ getHandlers().forEach(dch -> dch.dataUpdated(index, 1));
+ return prev;
+ }
+
+ @Override
+ public void add(int index, T element) {
+ ds.add(index, element);
+ getHandlers().forEach(dch -> dch.dataAdded(index, 1));
+ }
+
+ @Override
+ public T remove(int index) {
+ T removed = ds.remove(index);
+ getHandlers().forEach(dch -> dch.dataRemoved(index, 1));
+ return removed;
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ return ds.indexOf(o);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ return ds.lastIndexOf(o);
+ }
+
+ @Override
+ public ListIterator<T> listIterator() {
+ // TODO could be implemented by a custom iterator.
+ throw new UnsupportedOperationException(
+ "List iterators not supported at this time.");
+ }
+
+ @Override
+ public ListIterator<T> listIterator(int index) {
+ // TODO could be implemented by a custom iterator.
+ throw new UnsupportedOperationException(
+ "List iterators not supported at this time.");
+ }
+
+ @Override
+ public List<T> subList(int fromIndex, int toIndex) {
+ throw new UnsupportedOperationException("Sub lists not supported.");
+ }
+ }
+
+ /**
+ * Iterator returned by {@link ListWrapper}
+ */
+ private class ListWrapperIterator implements Iterator<T> {
+
+ private final Iterator<T> iterator;
+
+ /**
+ * Constructs a new iterator
+ */
+ public ListWrapperIterator(Iterator<T> iterator) {
+ this.iterator = iterator;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public T next() {
+ return iterator.next();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException(
+ "Iterator.remove() is not supported by this iterator.");
+ }
+ }
+
+ /**
+ * Datasource for providing row pojo's
+ */
+ private final List<T> ds;
+
+ /**
+ * Wrapper that wraps the data source
+ */
+ private final ListWrapper wrapper;
+
+ /**
+ * Handler for listening to changes in the underlying list.
+ */
+ private Set<DataChangeHandler> changeHandlers = new LinkedHashSet<>();
+
+ /**
+ * Constructs a new list data source.
+ * <p>
+ * Note: Modifications to the original list will not be reflected in the
+ * data source after the data source has been constructed. To add or remove
+ * items to the data source after it has been constructed use
+ * {@link ListDataSource#asList()}.
+ *
+ *
+ * @param datasource
+ * The list to use for providing the data to the grid
+ */
+ public ListDataSource(List<T> datasource) {
+ if (datasource == null) {
+ throw new IllegalArgumentException("datasource cannot be null");
+ }
+ ds = new ArrayList<T>(datasource);
+ wrapper = new ListWrapper();
+ }
+
+ /**
+ * Constructs a data source with a set of rows. You can dynamically add and
+ * remove rows from the data source via the list you get from
+ * {@link ListDataSource#asList()}
+ *
+ * @param rows
+ * The rows to initially add to the data source
+ */
+ public ListDataSource(T... rows) {
+ if (rows == null) {
+ ds = new ArrayList<T>();
+ } else {
+ ds = new ArrayList<T>(Arrays.asList(rows));
+ }
+ wrapper = new ListWrapper();
+ }
+
+ @Override
+ public void ensureAvailability(int firstRowIndex, int numberOfRows) {
+ if (firstRowIndex >= ds.size()) {
+ throw new IllegalStateException(
+ "Trying to fetch rows outside of array");
+ }
+
+ getHandlers()
+ .forEach(dch -> dch.dataAvailable(firstRowIndex, numberOfRows));
+ }
+
+ @Override
+ public T getRow(int rowIndex) {
+ return ds.get(rowIndex);
+ }
+
+ @Override
+ public int size() {
+ return ds.size();
+ }
+
+ @Override
+ public Registration addDataChangeHandler(
+ DataChangeHandler dataChangeHandler) {
+ Objects.requireNonNull(dataChangeHandler,
+ "DataChangeHandler can't be null");
+ changeHandlers.add(dataChangeHandler);
+ return () -> changeHandlers.remove(dataChangeHandler);
+ }
+
+ /**
+ * Gets the list that backs this datasource. Any changes made to this list
+ * will be reflected in the datasource.
+ * <p>
+ * Note: The list is not the same list as passed into the data source via
+ * the constructor.
+ *
+ * @return Returns a list implementation that wraps the real list that backs
+ * the data source and provides events for the data source
+ * listeners.
+ */
+ public List<T> asList() {
+ return wrapper;
+ }
+
+ @Override
+ public RowHandle<T> getHandle(T row) throws IllegalStateException {
+ assert ds.contains(row) : "This data source doesn't contain the row "
+ + row;
+ return new RowHandleImpl(row);
+ }
+
+ /**
+ * Sort entire container according to a {@link Comparator}.
+ *
+ * @param comparator
+ * a comparator object, which compares two data source entries
+ * (beans/pojos)
+ */
+ public void sort(Comparator<T> comparator) {
+ Collections.sort(ds, comparator);
+ getHandlers().forEach(dch -> dch.dataUpdated(0, ds.size()));
+ }
+
+ /**
+ * Retrieves the index for given row object.
+ * <p>
+ * <em>Note:</em> This method does not verify that the given row object
+ * exists at all in this DataSource.
+ *
+ * @param row
+ * the row object
+ * @return index of the row; or <code>-1</code> if row is not available
+ */
+ public int indexOf(T row) {
+ return ds.indexOf(row);
+ }
+
+ /**
+ * Returns a {@link SelectAllHandler} for this ListDataSource.
+ *
+ * @return select all handler
+ */
+ public SelectAllHandler<T> getSelectAllHandler() {
+ return new SelectAllHandler<T>() {
+ @Override
+ public void onSelectAll(SelectAllEvent<T> event) {
+ event.getSelectionModel().select(asList());
+ }
+ };
+ }
+
+ private Stream<DataChangeHandler> getHandlers() {
+ Set<DataChangeHandler> copy = new LinkedHashSet<>(changeHandlers);
+ return copy.stream();
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/datasources/ListSorter.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/datasources/ListSorter.java
new file mode 100644
index 0000000000..24d1eec06c
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/datasources/ListSorter.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.datasources;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.v7.client.widget.grid.sort.SortEvent;
+import com.vaadin.v7.client.widget.grid.sort.SortHandler;
+import com.vaadin.v7.client.widget.grid.sort.SortOrder;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Provides sorting facility from Grid for the {@link ListDataSource} in-memory
+ * data source.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * Grid row data type
+ * @since 7.4
+ */
+public class ListSorter<T> {
+
+ private Grid<T> grid;
+ private Map<Grid.Column<?, T>, Comparator<?>> comparators;
+ private HandlerRegistration sortHandlerRegistration;
+
+ public ListSorter(Grid<T> grid) {
+
+ if (grid == null) {
+ throw new IllegalArgumentException("Grid can not be null");
+ }
+
+ this.grid = grid;
+ comparators = new HashMap<Grid.Column<?, T>, Comparator<?>>();
+
+ sortHandlerRegistration = grid.addSortHandler(new SortHandler<T>() {
+ @Override
+ public void sort(SortEvent<T> event) {
+ ListSorter.this.sort(event.getOrder());
+ }
+ });
+ }
+
+ /**
+ * Detach this Sorter from the Grid. This unregisters the sort event handler
+ * which was used to apply sorting to the ListDataSource.
+ */
+ public void removeFromGrid() {
+ sortHandlerRegistration.removeHandler();
+ }
+
+ /**
+ * Assign or remove a comparator for a column. This comparator method, if
+ * defined, is always used in favour of 'natural' comparison of objects
+ * (i.e. the compareTo of objects implementing the Comparable interface,
+ * which includes all standard data classes like String, Number derivatives
+ * and Dates). Any existing comparator can be removed by passing in a
+ * non-null GridColumn and a null Comparator.
+ *
+ * @param column
+ * a grid column. May not be null.
+ * @param comparator
+ * comparator method for the values returned by the grid column.
+ * If null, any existing comparator is removed.
+ */
+ public <C> void setComparator(Grid.Column<C, T> column,
+ Comparator<C> comparator) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Column reference can not be null");
+ }
+ if (comparator == null) {
+ comparators.remove(column);
+ } else {
+ comparators.put(column, comparator);
+ }
+ }
+
+ /**
+ * Retrieve the comparator assigned for a specific grid column.
+ *
+ * @param column
+ * a grid column. May not be null.
+ * @return a comparator, or null if no comparator for the specified grid
+ * column has been set.
+ */
+ @SuppressWarnings("unchecked")
+ public <C> Comparator<C> getComparator(Grid.Column<C, T> column) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Column reference can not be null");
+ }
+ return (Comparator<C>) comparators.get(column);
+ }
+
+ /**
+ * Remove all comparator mappings. Useful if the data source has changed but
+ * this Sorter is being re-used.
+ */
+ public void clearComparators() {
+ comparators.clear();
+ }
+
+ /**
+ * Apply sorting to the current ListDataSource.
+ *
+ * @param order
+ * the sort order list provided by the grid sort event
+ */
+ private void sort(final List<SortOrder> order) {
+ DataSource<T> ds = grid.getDataSource();
+ if (!(ds instanceof ListDataSource)) {
+ throw new IllegalStateException(
+ "Grid " + grid + " data source is not a ListDataSource!");
+ }
+
+ ((ListDataSource<T>) ds).sort(new Comparator<T>() {
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public int compare(T a, T b) {
+
+ for (SortOrder o : order) {
+
+ Grid.Column column = o.getColumn();
+ Comparator cmp = ListSorter.this.comparators.get(column);
+ int result = 0;
+ Object value_a = column.getValue(a);
+ Object value_b = column.getValue(b);
+ if (cmp != null) {
+ result = cmp.compare(value_a, value_b);
+ } else {
+ if (!(value_a instanceof Comparable)) {
+ throw new IllegalStateException("Column " + column
+ + " has no assigned comparator and value "
+ + value_a + " isn't naturally comparable");
+ }
+ result = ((Comparable) value_a).compareTo(value_b);
+ }
+
+ if (result != 0) {
+ return o.getDirection() == SortDirection.ASCENDING
+ ? result : -result;
+ }
+ }
+
+ if (order.size() > 0) {
+ return order.get(0)
+ .getDirection() == SortDirection.ASCENDING
+ ? a.hashCode() - b.hashCode()
+ : b.hashCode() - a.hashCode();
+ }
+ return a.hashCode() - b.hashCode();
+ }
+ });
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/AbstractGridKeyEventHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/AbstractGridKeyEventHandler.java
new file mode 100644
index 0000000000..df997fd7ac
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/AbstractGridKeyEventHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.vaadin.v7.client.widgets.Grid.AbstractGridKeyEvent;
+
+/**
+ * Base interface of all handlers for {@link AbstractGridKeyEvent}s.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract interface AbstractGridKeyEventHandler extends EventHandler {
+
+ public abstract interface GridKeyDownHandler
+ extends AbstractGridKeyEventHandler {
+ public void onKeyDown(GridKeyDownEvent event);
+ }
+
+ public abstract interface GridKeyUpHandler
+ extends AbstractGridKeyEventHandler {
+ public void onKeyUp(GridKeyUpEvent event);
+ }
+
+ public abstract interface GridKeyPressHandler
+ extends AbstractGridKeyEventHandler {
+ public void onKeyPress(GridKeyPressEvent event);
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/AbstractGridMouseEventHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/AbstractGridMouseEventHandler.java
new file mode 100644
index 0000000000..9622047133
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/AbstractGridMouseEventHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.vaadin.v7.client.widgets.Grid.AbstractGridMouseEvent;
+
+/**
+ * Base interface of all handlers for {@link AbstractGridMouseEvent}s.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract interface AbstractGridMouseEventHandler extends EventHandler {
+
+ public abstract interface GridClickHandler
+ extends AbstractGridMouseEventHandler {
+ public void onClick(GridClickEvent event);
+ }
+
+ public abstract interface GridDoubleClickHandler
+ extends AbstractGridMouseEventHandler {
+ public void onDoubleClick(GridDoubleClickEvent event);
+ }
+
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyClickHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyClickHandler.java
new file mode 100644
index 0000000000..e05d6c476e
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyClickHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler.GridClickHandler;
+
+/**
+ * Handler for {@link GridClickEvent}s that happen in the body of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyClickHandler extends GridClickHandler {
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyDoubleClickHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyDoubleClickHandler.java
new file mode 100644
index 0000000000..1c0bacebd0
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyDoubleClickHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler.GridDoubleClickHandler;
+
+/**
+ * Handler for {@link GridDoubleClickEvent}s that happen in the body of the
+ * Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyDoubleClickHandler extends GridDoubleClickHandler {
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyDownHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyDownHandler.java
new file mode 100644
index 0000000000..a2f2278b39
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when the focused cell is in
+ * the body of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyDownHandler extends GridKeyDownHandler {
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyPressHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyPressHandler.java
new file mode 100644
index 0000000000..4098685e89
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when the focused cell is
+ * in the body of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyPressHandler extends GridKeyPressHandler {
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyUpHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyUpHandler.java
new file mode 100644
index 0000000000..d3d7544ac7
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/BodyKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when the focused cell is in
+ * the body of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyUpHandler extends GridKeyUpHandler {
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnReorderEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnReorderEvent.java
new file mode 100644
index 0000000000..399ed128a2
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnReorderEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * An event for notifying that the columns in the Grid have been reordered.
+ *
+ * @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.5.0
+ * @author Vaadin Ltd
+ */
+public class ColumnReorderEvent<T> extends GwtEvent<ColumnReorderHandler<T>> {
+
+ /**
+ * Handler type.
+ */
+ private final static Type<ColumnReorderHandler<?>> TYPE = new Type<ColumnReorderHandler<?>>();
+
+ public static final Type<ColumnReorderHandler<?>> getType() {
+ return TYPE;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public Type<ColumnReorderHandler<T>> getAssociatedType() {
+ return (Type) TYPE;
+ }
+
+ @Override
+ protected void dispatch(ColumnReorderHandler<T> handler) {
+ handler.onColumnReorder(this);
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnReorderHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnReorderHandler.java
new file mode 100644
index 0000000000..65712f36a2
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnReorderHandler.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid column reorder event, called when the Grid's columns has
+ * been reordered.
+ *
+ * @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.5.0
+ * @author Vaadin Ltd
+ */
+public interface ColumnReorderHandler<T> extends EventHandler {
+
+ /**
+ * A column reorder event, fired by Grid when the columns of the Grid have
+ * been reordered.
+ *
+ * @param event
+ * column reorder event
+ */
+ public void onColumnReorder(ColumnReorderEvent<T> event);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnResizeEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnResizeEvent.java
new file mode 100644
index 0000000000..ba50893411
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnResizeEvent.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.v7.client.widgets.Grid.Column;
+
+/**
+ * An event for notifying that the columns in the Grid have been resized.
+ *
+ * @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.6
+ * @author Vaadin Ltd
+ */
+public class ColumnResizeEvent<T> extends GwtEvent<ColumnResizeHandler<T>> {
+
+ /**
+ * Handler type.
+ */
+ private final static Type<ColumnResizeHandler<?>> TYPE = new Type<ColumnResizeHandler<?>>();
+
+ private Column<?, T> column;
+
+ /**
+ * @param column
+ */
+ public ColumnResizeEvent(Column<?, T> column) {
+ this.column = column;
+ }
+
+ public static final Type<ColumnResizeHandler<?>> getType() {
+ return TYPE;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public Type<ColumnResizeHandler<T>> getAssociatedType() {
+ return (Type) TYPE;
+ }
+
+ @Override
+ protected void dispatch(ColumnResizeHandler<T> handler) {
+ handler.onColumnResize(this);
+ }
+
+ /**
+ * @return the column
+ */
+ public Column<?, T> getColumn() {
+ return column;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnResizeHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnResizeHandler.java
new file mode 100644
index 0000000000..dd7da4a9ba
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnResizeHandler.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid column resize event, called when the Grid's columns has
+ * been resized.
+ *
+ * @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.6
+ * @author Vaadin Ltd
+ */
+public interface ColumnResizeHandler<T> extends EventHandler {
+
+ /**
+ * A column resize event, fired by Grid when the columns of the Grid have
+ * been resized.
+ *
+ * @param event
+ * column resize event
+ */
+ public void onColumnResize(ColumnResizeEvent<T> event);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnVisibilityChangeEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnVisibilityChangeEvent.java
new file mode 100644
index 0000000000..e58b678e3d
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnVisibilityChangeEvent.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.v7.client.widgets.Grid.Column;
+
+/**
+ * An event for notifying that the columns in the Grid's have changed
+ * visibility.
+ *
+ * @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.5.0
+ * @author Vaadin Ltd
+ */
+public class ColumnVisibilityChangeEvent<T>
+ extends GwtEvent<ColumnVisibilityChangeHandler<T>> {
+
+ private final static Type<ColumnVisibilityChangeHandler<?>> TYPE = new Type<ColumnVisibilityChangeHandler<?>>();
+
+ public static final Type<ColumnVisibilityChangeHandler<?>> getType() {
+ return TYPE;
+ }
+
+ private final Column<?, T> column;
+
+ private final boolean userOriginated;
+
+ private final boolean hidden;
+
+ public ColumnVisibilityChangeEvent(Column<?, T> column, boolean hidden,
+ boolean userOriginated) {
+ this.column = column;
+ this.hidden = hidden;
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Returns the column where the visibility change occurred.
+ *
+ * @return the column where the visibility change occurred.
+ */
+ public Column<?, T> 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;
+ }
+
+ /**
+ * Is the visibility change triggered by user.
+ *
+ * @return <code>true</code> if the change was triggered by user,
+ * <code>false</code> if not
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public com.google.gwt.event.shared.GwtEvent.Type<ColumnVisibilityChangeHandler<T>> getAssociatedType() {
+ return (Type) TYPE;
+ }
+
+ @Override
+ protected void dispatch(ColumnVisibilityChangeHandler<T> handler) {
+ handler.onVisibilityChange(this);
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnVisibilityChangeHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnVisibilityChangeHandler.java
new file mode 100644
index 0000000000..0ef91f5d79
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ColumnVisibilityChangeHandler.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid column visibility change event, called when the Grid's
+ * columns have changed visibility to hidden or visible.
+ *
+ * @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.5.0
+ * @author Vaadin Ltd
+ */
+public interface ColumnVisibilityChangeHandler<T> extends EventHandler {
+
+ /**
+ * A column visibility change event, fired by Grid when a column in the Grid
+ * has changed visibility.
+ *
+ * @param event
+ * column visibility change event
+ */
+ public void onVisibilityChange(ColumnVisibilityChangeEvent<T> event);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterClickHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterClickHandler.java
new file mode 100644
index 0000000000..4e04a181e4
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterClickHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler.GridClickHandler;
+
+/**
+ * Handler for {@link GridClickEvent}s that happen in the footer of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterClickHandler extends GridClickHandler {
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterDoubleClickHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterDoubleClickHandler.java
new file mode 100644
index 0000000000..499d77879c
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterDoubleClickHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler.GridDoubleClickHandler;
+
+/**
+ * Handler for {@link GridDoubleClickEvent}s that happen in the footer of the
+ * Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterDoubleClickHandler extends GridDoubleClickHandler {
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyDownHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyDownHandler.java
new file mode 100644
index 0000000000..22871d8278
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when the focused cell is in
+ * the footer of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyDownHandler extends GridKeyDownHandler {
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyPressHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyPressHandler.java
new file mode 100644
index 0000000000..5769544b2c
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when the focused cell is
+ * in the footer of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyPressHandler extends GridKeyPressHandler {
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyUpHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyUpHandler.java
new file mode 100644
index 0000000000..565c58cd27
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/FooterKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when the focused cell is in
+ * the footer of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyUpHandler extends GridKeyUpHandler {
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridClickEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridClickEvent.java
new file mode 100644
index 0000000000..1dfd1dc42d
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridClickEvent.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler.GridClickHandler;
+import com.vaadin.v7.client.widgets.Grid;
+import com.vaadin.v7.client.widgets.Grid.AbstractGridMouseEvent;
+
+/**
+ * Represents native mouse click event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridClickEvent extends AbstractGridMouseEvent<GridClickHandler> {
+
+ public GridClickEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.CLICK;
+ }
+
+ @Override
+ protected void doDispatch(GridClickHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyClickHandler)
+ || (section == Section.HEADER
+ && handler instanceof HeaderClickHandler)
+ || (section == Section.FOOTER
+ && handler instanceof FooterClickHandler)) {
+ handler.onClick(this);
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridDoubleClickEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridDoubleClickEvent.java
new file mode 100644
index 0000000000..f8383303aa
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridDoubleClickEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler.GridDoubleClickHandler;
+import com.vaadin.v7.client.widgets.Grid;
+import com.vaadin.v7.client.widgets.Grid.AbstractGridMouseEvent;
+
+/**
+ * Represents native mouse double click event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridDoubleClickEvent
+ extends AbstractGridMouseEvent<GridDoubleClickHandler> {
+
+ public GridDoubleClickEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.DBLCLICK;
+ }
+
+ @Override
+ protected void doDispatch(GridDoubleClickHandler handler, Section section) {
+ if ((section == Section.BODY
+ && handler instanceof BodyDoubleClickHandler)
+ || (section == Section.HEADER
+ && handler instanceof HeaderDoubleClickHandler)
+ || (section == Section.FOOTER
+ && handler instanceof FooterDoubleClickHandler)) {
+ handler.onDoubleClick(this);
+ }
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridEnabledEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridEnabledEvent.java
new file mode 100644
index 0000000000..7d563329a5
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridEnabledEvent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * An enabled/disabled event, fired by the Grid when it is disabled or enabled.
+ *
+ * @since 7.7
+ * @author Vaadin Ltd
+ */
+public class GridEnabledEvent extends GwtEvent<GridEnabledHandler> {
+ /**
+ * The type of this event
+ */
+ public static final Type<GridEnabledHandler> TYPE = new Type<GridEnabledHandler>();
+ private final boolean enabled;
+
+ public GridEnabledEvent(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public Type<GridEnabledHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(final GridEnabledHandler handler) {
+ handler.onEnabled(enabled);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridEnabledHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridEnabledHandler.java
new file mode 100644
index 0000000000..88d9283f07
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridEnabledHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid enabled/disabled event, called when the Grid is enabled or
+ * disabled.
+ *
+ * @since 7.7
+ * @author Vaadin Ltd
+ */
+public interface GridEnabledHandler extends EventHandler {
+
+ /**
+ * Called when Grid is enabled or disabled.
+ *
+ * @param enabled
+ * true if status changes from disabled to enabled, otherwise
+ * false.
+ */
+ public void onEnabled(boolean enabled);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyDownEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyDownEvent.java
new file mode 100644
index 0000000000..3afd060307
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyDownEvent.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyDownHandler;
+import com.vaadin.v7.client.widgets.Grid;
+import com.vaadin.v7.client.widgets.Grid.AbstractGridKeyEvent;
+
+/**
+ * Represents native key down event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridKeyDownEvent extends AbstractGridKeyEvent<GridKeyDownHandler> {
+
+ public GridKeyDownEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected void doDispatch(GridKeyDownHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyKeyDownHandler)
+ || (section == Section.HEADER
+ && handler instanceof HeaderKeyDownHandler)
+ || (section == Section.FOOTER
+ && handler instanceof FooterKeyDownHandler)) {
+ handler.onKeyDown(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYDOWN;
+ }
+
+ /**
+ * Does the key code represent an arrow key?
+ *
+ * @param keyCode
+ * the key code
+ * @return if it is an arrow key code
+ */
+ public static boolean isArrow(int keyCode) {
+ switch (keyCode) {
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_RIGHT:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_LEFT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Gets the native key code. These key codes are enumerated in the
+ * {@link KeyCodes} class.
+ *
+ * @return the key code
+ */
+ public int getNativeKeyCode() {
+ return getNativeEvent().getKeyCode();
+ }
+
+ /**
+ * Is this a key down arrow?
+ *
+ * @return whether this is a down arrow key event
+ */
+ public boolean isDownArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Is this a left arrow?
+ *
+ * @return whether this is a left arrow key event
+ */
+ public boolean isLeftArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Is this a right arrow?
+ *
+ * @return whether this is a right arrow key event
+ */
+ public boolean isRightArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Is this a up arrow?
+ *
+ * @return whether this is a right arrow key event
+ */
+ public boolean isUpArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_UP;
+ }
+
+ @Override
+ public String toDebugString() {
+ return super.toDebugString() + "[" + getNativeKeyCode() + "]";
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyPressEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyPressEvent.java
new file mode 100644
index 0000000000..3fb3bb94e5
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyPressEvent.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyPressHandler;
+import com.vaadin.v7.client.widgets.Grid;
+import com.vaadin.v7.client.widgets.Grid.AbstractGridKeyEvent;
+
+/**
+ * Represents native key press event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridKeyPressEvent
+ extends AbstractGridKeyEvent<GridKeyPressHandler> {
+
+ public GridKeyPressEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected void doDispatch(GridKeyPressHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyKeyPressHandler)
+ || (section == Section.HEADER
+ && handler instanceof HeaderKeyPressHandler)
+ || (section == Section.FOOTER
+ && handler instanceof FooterKeyPressHandler)) {
+ handler.onKeyPress(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYPRESS;
+ }
+
+ /**
+ * Gets the char code for this event.
+ *
+ * @return the char code
+ */
+ public char getCharCode() {
+ return (char) getUnicodeCharCode();
+ }
+
+ /**
+ * Gets the Unicode char code (code point) for this event.
+ *
+ * @return the Unicode char code
+ */
+ public int getUnicodeCharCode() {
+ return getNativeEvent().getCharCode();
+ }
+
+ @Override
+ public String toDebugString() {
+ return super.toDebugString() + "[" + getCharCode() + "]";
+ }
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyUpEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyUpEvent.java
new file mode 100644
index 0000000000..485468b879
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/GridKeyUpEvent.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyUpHandler;
+import com.vaadin.v7.client.widgets.Grid;
+import com.vaadin.v7.client.widgets.Grid.AbstractGridKeyEvent;
+
+/**
+ * Represents native key up event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridKeyUpEvent extends AbstractGridKeyEvent<GridKeyUpHandler> {
+
+ public GridKeyUpEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected void doDispatch(GridKeyUpHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyKeyUpHandler)
+ || (section == Section.HEADER
+ && handler instanceof HeaderKeyUpHandler)
+ || (section == Section.FOOTER
+ && handler instanceof FooterKeyUpHandler)) {
+ handler.onKeyUp(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYUP;
+ }
+
+ /**
+ * Does the key code represent an arrow key?
+ *
+ * @param keyCode
+ * the key code
+ * @return if it is an arrow key code
+ */
+ public static boolean isArrow(int keyCode) {
+ switch (keyCode) {
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_RIGHT:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_LEFT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Gets the native key code. These key codes are enumerated in the
+ * {@link KeyCodes} class.
+ *
+ * @return the key code
+ */
+ public int getNativeKeyCode() {
+ return getNativeEvent().getKeyCode();
+ }
+
+ /**
+ * Is this a key down arrow?
+ *
+ * @return whether this is a down arrow key event
+ */
+ public boolean isDownArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Is this a left arrow?
+ *
+ * @return whether this is a left arrow key event
+ */
+ public boolean isLeftArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Is this a right arrow?
+ *
+ * @return whether this is a right arrow key event
+ */
+ public boolean isRightArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Is this a up arrow?
+ *
+ * @return whether this is a right arrow key event
+ */
+ public boolean isUpArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_UP;
+ }
+
+ @Override
+ public String toDebugString() {
+ return super.toDebugString() + "[" + getNativeKeyCode() + "]";
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderClickHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderClickHandler.java
new file mode 100644
index 0000000000..c016acbd17
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderClickHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler.GridClickHandler;
+
+/**
+ * Handler for {@link GridClickEvent}s that happen in the header of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderClickHandler extends GridClickHandler {
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderDoubleClickHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderDoubleClickHandler.java
new file mode 100644
index 0000000000..cd285dd1f3
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderDoubleClickHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler.GridDoubleClickHandler;
+
+/**
+ * Handler for {@link GridDoubleClickEvent}s that happen in the header of the
+ * Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderDoubleClickHandler extends GridDoubleClickHandler {
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyDownHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyDownHandler.java
new file mode 100644
index 0000000000..690ca050a9
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when the focused cell is in
+ * the header of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyDownHandler extends GridKeyDownHandler {
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyPressHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyPressHandler.java
new file mode 100644
index 0000000000..1d012b11cc
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when the focused cell is
+ * in the header of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyPressHandler extends GridKeyPressHandler {
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyUpHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyUpHandler.java
new file mode 100644
index 0000000000..77672b4d0a
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/HeaderKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when the focused cell is in
+ * the header of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyUpHandler extends GridKeyUpHandler {
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ScrollEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ScrollEvent.java
new file mode 100644
index 0000000000..2dfd6f6693
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ScrollEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * An event that signifies that a scrollbar bundle has been scrolled
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class ScrollEvent extends GwtEvent<ScrollHandler> {
+
+ /** The type of this event */
+ public static final Type<ScrollHandler> TYPE = new Type<ScrollHandler>();
+
+ @Override
+ public Type<ScrollHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(final ScrollHandler handler) {
+ handler.onScroll(this);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ScrollHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ScrollHandler.java
new file mode 100644
index 0000000000..1e805ce653
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/ScrollHandler.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * A handler that gets called whenever a scrollbar bundle is scrolled
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public interface ScrollHandler extends EventHandler {
+ /**
+ * A callback method that is called once a scrollbar bundle has been
+ * scrolled.
+ *
+ * @param event
+ * the scroll event
+ */
+ public void onScroll(ScrollEvent event);
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/SelectAllEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/SelectAllEvent.java
new file mode 100644
index 0000000000..c46a192292
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/SelectAllEvent.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel;
+
+/**
+ * A select all event, fired by the Grid when it needs all rows in data source
+ * to be selected.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SelectAllEvent<T> extends GwtEvent<SelectAllHandler<T>> {
+
+ /**
+ * Handler type.
+ */
+ private final static Type<SelectAllHandler<?>> TYPE = new Type<SelectAllHandler<?>>();;
+
+ private SelectionModel.Multi<T> selectionModel;
+
+ public SelectAllEvent(SelectionModel.Multi<T> selectionModel) {
+ this.selectionModel = selectionModel;
+ }
+
+ public static final Type<SelectAllHandler<?>> getType() {
+ return TYPE;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public Type<SelectAllHandler<T>> getAssociatedType() {
+ return (Type) TYPE;
+ }
+
+ @Override
+ protected void dispatch(SelectAllHandler<T> handler) {
+ handler.onSelectAll(this);
+ }
+
+ public SelectionModel.Multi<T> getSelectionModel() {
+ return selectionModel;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/SelectAllHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/SelectAllHandler.java
new file mode 100644
index 0000000000..035de15447
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/events/SelectAllHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid select all event, called when the Grid needs all rows in
+ * data source to be selected.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface SelectAllHandler<T> extends EventHandler {
+
+ /**
+ * Called when select all value in SelectionColumn header changes value.
+ *
+ * @param event
+ * select all event telling that all rows should be selected
+ */
+ public void onSelectAll(SelectAllEvent<T> event);
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/AbstractRowHandleSelectionModel.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/AbstractRowHandleSelectionModel.java
new file mode 100644
index 0000000000..41e85929aa
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/AbstractRowHandleSelectionModel.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+
+/**
+ * An abstract class that adds a consistent API for common methods that's needed
+ * by Vaadin's server-based selection models to work.
+ * <p>
+ * <em>Note:</em> This should be an interface instead of an abstract class, if
+ * only we could define protected methods in an interface.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * The grid's row type
+ * @since 7.4
+ */
+public abstract class AbstractRowHandleSelectionModel<T>
+ implements SelectionModel<T> {
+ /**
+ * Select a row, based on its
+ * {@link com.vaadin.client.data.DataSource.RowHandle RowHandle}.
+ * <p>
+ * <em>Note:</em> this method may not fire selection change events.
+ *
+ * @param handle
+ * the handle to select by
+ * @return <code>true</code> iff the selection state was changed by this
+ * call
+ * @throws UnsupportedOperationException
+ * if the selection model does not support either handles or
+ * selection
+ */
+ protected abstract boolean selectByHandle(RowHandle<T> handle);
+
+ /**
+ * Deselect a row, based on its
+ * {@link com.vaadin.client.data.DataSource.RowHandle RowHandle}.
+ * <p>
+ * <em>Note:</em> this method may not fire selection change events.
+ *
+ * @param handle
+ * the handle to deselect by
+ * @return <code>true</code> iff the selection state was changed by this
+ * call
+ * @throws UnsupportedOperationException
+ * if the selection model does not support either handles or
+ * deselection
+ */
+ protected abstract boolean deselectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException;
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/ClickSelectHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/ClickSelectHandler.java
new file mode 100644
index 0000000000..588d8a354b
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/ClickSelectHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.v7.client.widget.grid.events.BodyClickHandler;
+import com.vaadin.v7.client.widget.grid.events.GridClickEvent;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Generic class to perform selections when clicking on cells in body of Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ClickSelectHandler<T> {
+
+ private Grid<T> grid;
+ private HandlerRegistration clickHandler;
+ private boolean deselectAllowed = true;
+
+ private class RowClickHandler implements BodyClickHandler {
+
+ @Override
+ public void onClick(GridClickEvent event) {
+ T row = (T) event.getTargetCell().getRow();
+ if (!grid.isSelected(row)) {
+ grid.select(row);
+ } else if (deselectAllowed) {
+ grid.deselect(row);
+ }
+ }
+ }
+
+ /**
+ * Constructor for ClickSelectHandler. This constructor will add all
+ * necessary handlers for selecting rows by clicking cells.
+ *
+ * @param grid
+ * grid to attach to
+ */
+ public ClickSelectHandler(Grid<T> grid) {
+ this.grid = grid;
+ clickHandler = grid.addBodyClickHandler(new RowClickHandler());
+ }
+
+ /**
+ * Clean up function for removing all now obsolete handlers.
+ */
+ public void removeHandler() {
+ clickHandler.removeHandler();
+ }
+
+ /**
+ * Sets whether clicking the currently selected row should deselect the row.
+ *
+ * @param deselectAllowed
+ * <code>true</code> to allow deselecting the selected row;
+ * otherwise <code>false</code>
+ */
+ public void setDeselectAllowed(boolean deselectAllowed) {
+ this.deselectAllowed = deselectAllowed;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/HasSelectionHandlers.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/HasSelectionHandlers.java
new file mode 100644
index 0000000000..b8793f671e
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/HasSelectionHandlers.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+
+/**
+ * Marker interface for widgets that fires selection events.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public interface HasSelectionHandlers<T> {
+
+ /**
+ * Register a selection change handler.
+ * <p>
+ * This handler is called whenever a
+ * {@link com.vaadin.ui.components.grid.selection.SelectionModel
+ * SelectionModel} detects a change in selection state.
+ *
+ * @param handler
+ * a {@link SelectionHandler}
+ * @return a handler registration object, which can be used to remove the
+ * handler.
+ */
+ public HandlerRegistration addSelectionHandler(SelectionHandler<T> handler);
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/MultiSelectionRenderer.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/MultiSelectionRenderer.java
new file mode 100644
index 0000000000..678d37369f
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/MultiSelectionRenderer.java
@@ -0,0 +1,781 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+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.GWT;
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.TouchStartEvent;
+import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.ui.CheckBox;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.v7.client.renderers.ClickableRenderer;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+import com.vaadin.v7.client.widget.grid.events.GridEnabledEvent;
+import com.vaadin.v7.client.widget.grid.events.GridEnabledHandler;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Multi.Batched;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Renderer showing multi selection check boxes.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the type of the associated grid
+ * @since 7.4
+ */
+public class MultiSelectionRenderer<T>
+ extends ClickableRenderer<Boolean, CheckBox> {
+
+ private static final String SELECTION_CHECKBOX_CLASSNAME = "-selection-checkbox";
+
+ /** The size of the autoscroll area, both top and bottom. */
+ private static final int SCROLL_AREA_GRADIENT_PX = 100;
+
+ /** The maximum number of pixels per second to autoscroll. */
+ private static final int SCROLL_TOP_SPEED_PX_SEC = 500;
+
+ /**
+ * The minimum area where the grid doesn't scroll while the pointer is
+ * pressed.
+ */
+ private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50;
+
+ /**
+ * Handler for MouseDown and TouchStart events for selection checkboxes.
+ *
+ * @since 7.5
+ */
+ private final class CheckBoxEventHandler implements MouseDownHandler,
+ TouchStartHandler, ClickHandler, GridEnabledHandler {
+ private final CheckBox checkBox;
+
+ /**
+ * @param checkBox
+ * checkbox widget for this handler
+ */
+ private CheckBoxEventHandler(CheckBox checkBox) {
+ this.checkBox = checkBox;
+ }
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ if (checkBox.isEnabled()) {
+ if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
+ startDragSelect(event.getNativeEvent(),
+ checkBox.getElement());
+ }
+ }
+ }
+
+ @Override
+ public void onTouchStart(TouchStartEvent event) {
+ if (checkBox.isEnabled()) {
+ startDragSelect(event.getNativeEvent(), checkBox.getElement());
+ }
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ // Clicking is already handled with MultiSelectionRenderer
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ @Override
+ public void onEnabled(boolean enabled) {
+ checkBox.setEnabled(enabled);
+ }
+ }
+
+ /**
+ * This class's main objective is to listen when to stop autoscrolling, and
+ * make sure everything stops accordingly.
+ */
+ private class TouchEventHandler implements NativePreviewHandler {
+ @Override
+ public void onPreviewNativeEvent(final NativePreviewEvent event) {
+ switch (event.getTypeInt()) {
+ case Event.ONTOUCHSTART: {
+ if (event.getNativeEvent().getTouches().length() == 1) {
+ /*
+ * Something has dropped a touchend/touchcancel and the
+ * scroller is most probably running amok. Let's cancel it
+ * and pretend that everything's going as expected
+ *
+ * Because this is a preview, this code is run before the
+ * event handler in MultiSelectionRenderer.onBrowserEvent.
+ * Therefore, we can simply kill everything and let that
+ * method restart things as they should.
+ */
+ autoScrollHandler.stop();
+
+ /*
+ * Related TODO: investigate why iOS seems to ignore a
+ * touchend/touchcancel when frames are dropped, and/or if
+ * something can be done about that.
+ */
+ }
+ break;
+ }
+
+ case Event.ONTOUCHMOVE:
+ event.cancel();
+ break;
+
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ /*
+ * Remember: targetElement is always where touchstart started,
+ * not where the finger is pointing currently.
+ */
+ final Element targetElement = Element
+ .as(event.getNativeEvent().getEventTarget());
+ if (isInFirstColumn(targetElement)) {
+ removeNativeHandler();
+ event.cancel();
+ }
+ break;
+ }
+ }
+
+ private boolean isInFirstColumn(final Element element) {
+ if (element == null) {
+ return false;
+ }
+ final Element tbody = getTbodyElement();
+
+ if (tbody == null || !tbody.isOrHasChild(element)) {
+ return false;
+ }
+
+ /*
+ * The null-parent in the while clause is in the case where element
+ * is an immediate tr child in the tbody. Should never happen in
+ * internal code, but hey...
+ */
+ Element cursor = element;
+ while (cursor.getParentElement() != null
+ && cursor.getParentElement().getParentElement() != tbody) {
+ cursor = cursor.getParentElement();
+ }
+
+ final Element tr = cursor.getParentElement();
+ return tr.getFirstChildElement().equals(cursor);
+ }
+ }
+
+ /**
+ * This class's responsibility is to
+ * <ul>
+ * <li>scroll the table while a pointer is kept in a scrolling zone and
+ * <li>select rows whenever a pointer is "activated" on a selection cell
+ * </ul>
+ * <p>
+ * <em>Techical note:</em> This class is an AnimationCallback because we
+ * need a timer: when the finger is kept in place while the grid scrolls, we
+ * still need to be able to make new selections. So, instead of relying on
+ * events (which won't be fired, since the pointer isn't necessarily
+ * moving), we do this check on each frame while the pointer is "active"
+ * (mouse is pressed, finger is on screen).
+ */
+ private class AutoScrollerAndSelector implements AnimationCallback {
+
+ /**
+ * If the acceleration gradient area is smaller than this, autoscrolling
+ * will be disabled (it becomes too quick to accelerate to be usable).
+ */
+ private static final int GRADIENT_MIN_THRESHOLD_PX = 10;
+
+ /**
+ * The speed at which the gradient area recovers, once scrolling in that
+ * direction has started.
+ */
+ private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1;
+ private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC
+ / 1000.0d;
+
+ /**
+ * The lowest y-coordinate on the {@link Event#getClientY() client} from
+ * where we need to start scrolling towards the top.
+ */
+ private int topBound = -1;
+
+ /**
+ * The highest y-coordinate on the {@link Event#getClientY() client}
+ * from where we need to scrolling towards the bottom.
+ */
+ private int bottomBound = -1;
+
+ /**
+ * <code>true</code> if the pointer is selecting, <code>false</code> if
+ * the pointer is deselecting.
+ */
+ private final boolean selectionPaint;
+
+ /**
+ * The area where the selection acceleration takes place. If &lt;
+ * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled
+ */
+ private final int gradientArea;
+
+ /**
+ * The number of pixels per seconds we currently are scrolling (negative
+ * is towards the top, positive is towards the bottom).
+ */
+ private double scrollSpeed = 0;
+
+ private double prevTimestamp = 0;
+
+ /**
+ * This field stores fractions of pixels to scroll, to make sure that
+ * we're able to scroll less than one px per frame.
+ */
+ private double pixelsToScroll = 0.0d;
+
+ /** Should this animator be running. */
+ private boolean running = false;
+
+ /** The handle in which this instance is running. */
+ private AnimationHandle handle;
+
+ /** The pointer's pageX coordinate of the first click. */
+ private int initialPageX = -1;
+
+ /** The pointer's pageY coordinate. */
+ private int pageY;
+
+ /** The logical index of the row that was most recently modified. */
+ private int lastModifiedLogicalRow = -1;
+
+ /** @see #doScrollAreaChecks(int) */
+ private int finalTopBound;
+
+ /** @see #doScrollAreaChecks(int) */
+ private int finalBottomBound;
+
+ private boolean scrollAreaShouldRebound = false;
+
+ private final int bodyAbsoluteTop;
+ private final int bodyAbsoluteBottom;
+
+ public AutoScrollerAndSelector(final int topBound,
+ final int bottomBound, final int gradientArea,
+ final boolean selectionPaint) {
+ finalTopBound = topBound;
+ finalBottomBound = bottomBound;
+ this.gradientArea = gradientArea;
+ this.selectionPaint = selectionPaint;
+
+ bodyAbsoluteTop = getBodyClientTop();
+ bodyAbsoluteBottom = getBodyClientBottom();
+ }
+
+ @Override
+ public void execute(final double timestamp) {
+ final double timeDiff = timestamp - prevTimestamp;
+ prevTimestamp = timestamp;
+
+ reboundScrollArea(timeDiff);
+
+ pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d);
+ final int intPixelsToScroll = (int) pixelsToScroll;
+ pixelsToScroll -= intPixelsToScroll;
+
+ if (intPixelsToScroll != 0) {
+ grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll);
+ }
+
+ int constrainedPageY = Math.max(bodyAbsoluteTop,
+ Math.min(bodyAbsoluteBottom, pageY));
+ int logicalRow = getLogicalRowIndex(WidgetUtil
+ .getElementFromPoint(initialPageX, constrainedPageY));
+
+ int incrementOrDecrement = (logicalRow > lastModifiedLogicalRow) ? 1
+ : -1;
+
+ /*
+ * Both pageY and initialPageX have their initialized (and
+ * unupdated) values while the cursor hasn't moved since the first
+ * invocation. This will lead to logicalRow being -1, until the
+ * pointer has been moved.
+ */
+ while (logicalRow != -1 && lastModifiedLogicalRow != logicalRow) {
+ lastModifiedLogicalRow += incrementOrDecrement;
+ setSelected(lastModifiedLogicalRow, selectionPaint);
+ }
+
+ reschedule();
+ }
+
+ /**
+ * If the scroll are has been offset by the pointer starting out there,
+ * move it back a bit
+ */
+ private void reboundScrollArea(double timeDiff) {
+ if (!scrollAreaShouldRebound) {
+ return;
+ }
+
+ int reboundPx = (int) Math
+ .ceil(SCROLL_AREA_REBOUND_PX_PER_MS * timeDiff);
+ if (topBound < finalTopBound) {
+ topBound += reboundPx;
+ topBound = Math.min(topBound, finalTopBound);
+ updateScrollSpeed(pageY);
+ } else if (bottomBound > finalBottomBound) {
+ bottomBound -= reboundPx;
+ bottomBound = Math.max(bottomBound, finalBottomBound);
+ updateScrollSpeed(pageY);
+ }
+ }
+
+ private void updateScrollSpeed(final int pointerPageY) {
+
+ final double ratio;
+ if (pointerPageY < topBound) {
+ final double distance = pointerPageY - topBound;
+ ratio = Math.max(-1, distance / gradientArea);
+ }
+
+ else if (pointerPageY > bottomBound) {
+ final double distance = pointerPageY - bottomBound;
+ ratio = Math.min(1, distance / gradientArea);
+ }
+
+ else {
+ ratio = 0;
+ }
+
+ scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
+ }
+
+ public void start(int logicalRowIndex) {
+ running = true;
+ setSelected(logicalRowIndex, selectionPaint);
+ lastModifiedLogicalRow = logicalRowIndex;
+ reschedule();
+ }
+
+ public void stop() {
+ running = false;
+
+ if (handle != null) {
+ handle.cancel();
+ handle = null;
+ }
+ }
+
+ private void reschedule() {
+ if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) {
+ handle = AnimationScheduler.get().requestAnimationFrame(this,
+ grid.getElement());
+ }
+ }
+
+ public void updatePointerCoords(int pageX, int pageY) {
+ doScrollAreaChecks(pageY);
+ updateScrollSpeed(pageY);
+ this.pageY = pageY;
+
+ if (initialPageX == -1) {
+ initialPageX = pageX;
+ }
+ }
+
+ /**
+ * This method checks whether the first pointer event started in an area
+ * that would start scrolling immediately, and does some actions
+ * accordingly.
+ * <p>
+ * If it is, that scroll area will be offset "beyond" the pointer (above
+ * if pointer is towards the top, otherwise below).
+ * <p>
+ * <span style="font-size:smaller">*) This behavior will change in
+ * future patches (henrik paul 2.7.2014)</span>
+ */
+ private void doScrollAreaChecks(int pageY) {
+ /*
+ * The first run makes sure that neither scroll position is
+ * underneath the finger, but offset to either direction from
+ * underneath the pointer.
+ */
+ if (topBound == -1) {
+ topBound = Math.min(finalTopBound, pageY);
+ bottomBound = Math.max(finalBottomBound, pageY);
+ }
+
+ /*
+ * Subsequent runs make sure that the scroll area grows (but doesn't
+ * shrink) with the finger, but no further than the final bound.
+ */
+ else {
+ int oldTopBound = topBound;
+ if (topBound < finalTopBound) {
+ topBound = Math.max(topBound,
+ Math.min(finalTopBound, pageY));
+ }
+
+ int oldBottomBound = bottomBound;
+ if (bottomBound > finalBottomBound) {
+ bottomBound = Math.min(bottomBound,
+ Math.max(finalBottomBound, pageY));
+ }
+
+ final boolean topDidNotMove = oldTopBound == topBound;
+ final boolean bottomDidNotMove = oldBottomBound == bottomBound;
+ final boolean wasVerticalMovement = pageY != this.pageY;
+ scrollAreaShouldRebound = (topDidNotMove && bottomDidNotMove
+ && wasVerticalMovement);
+ }
+ }
+ }
+
+ /**
+ * This class makes sure that pointer movemenets are registered and
+ * delegated to the autoscroller so that it can:
+ * <ul>
+ * <li>modify the speed in which we autoscroll.
+ * <li>"paint" a new row with the selection.
+ * </ul>
+ * Essentially, when a pointer is pressed on the selection column, a native
+ * preview handler is registered (so that selection gestures can happen
+ * outside of the selection column). The handler itself makes sure that it's
+ * detached when the pointer is "lifted".
+ */
+ private class AutoScrollHandler {
+ private AutoScrollerAndSelector autoScroller;
+
+ /** The registration info for {@link #scrollPreviewHandler} */
+ private HandlerRegistration handlerRegistration;
+
+ private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() {
+ @Override
+ public void onPreviewNativeEvent(final NativePreviewEvent event) {
+ if (autoScroller == null) {
+ stop();
+ return;
+ }
+
+ final NativeEvent nativeEvent = event.getNativeEvent();
+ int pageY = 0;
+ int pageX = 0;
+ switch (event.getTypeInt()) {
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ pageY = WidgetUtil.getTouchOrMouseClientY(nativeEvent);
+ pageX = WidgetUtil.getTouchOrMouseClientX(nativeEvent);
+ autoScroller.updatePointerCoords(pageX, pageY);
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ stop();
+ break;
+ }
+ }
+ };
+
+ /**
+ * The top bound, as calculated from the {@link Event#getClientY()
+ * client} coordinates.
+ */
+ private int topBound = -1;
+
+ /**
+ * The bottom bound, as calculated from the {@link Event#getClientY()
+ * client} coordinates.
+ */
+ private int bottomBound = -1;
+
+ /** The size of the autoscroll acceleration area. */
+ private int gradientArea;
+
+ public void start(int logicalRowIndex) {
+
+ SelectionModel<T> model = grid.getSelectionModel();
+ if (model instanceof Batched) {
+ Batched<?> batchedModel = (Batched<?>) model;
+ batchedModel.startBatchSelect();
+ }
+
+ /*
+ * bounds are updated whenever the autoscroll cycle starts, to make
+ * sure that the widget hasn't changed in size, moved around, or
+ * whatnot.
+ */
+ updateScrollBounds();
+
+ assert handlerRegistration == null : "handlerRegistration was not null";
+ assert autoScroller == null : "autoScroller was not null";
+ handlerRegistration = Event
+ .addNativePreviewHandler(scrollPreviewHandler);
+
+ autoScroller = new AutoScrollerAndSelector(topBound, bottomBound,
+ gradientArea, !isSelected(logicalRowIndex));
+ autoScroller.start(logicalRowIndex);
+ }
+
+ private void updateScrollBounds() {
+ final int topBorder = getBodyClientTop();
+ final int bottomBorder = getBodyClientBottom();
+
+ topBound = topBorder + SCROLL_AREA_GRADIENT_PX;
+ bottomBound = bottomBorder - SCROLL_AREA_GRADIENT_PX;
+ gradientArea = SCROLL_AREA_GRADIENT_PX;
+
+ // modify bounds if they're too tightly packed
+ if (bottomBound - topBound < MIN_NO_AUTOSCROLL_AREA_PX) {
+ int adjustment = MIN_NO_AUTOSCROLL_AREA_PX
+ - (bottomBound - topBound);
+ topBound -= adjustment / 2;
+ bottomBound += adjustment / 2;
+ gradientArea -= adjustment / 2;
+ }
+ }
+
+ public void stop() {
+ if (handlerRegistration != null) {
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ }
+
+ if (autoScroller != null) {
+ autoScroller.stop();
+ autoScroller = null;
+ }
+
+ SelectionModel<T> model = grid.getSelectionModel();
+ if (model instanceof Batched) {
+ Batched<?> batchedModel = (Batched<?>) model;
+ batchedModel.commitBatchSelect();
+ }
+
+ removeNativeHandler();
+ }
+ }
+
+ private static final String LOGICAL_ROW_PROPERTY_INT = "vEscalatorLogicalRow";
+
+ private final Grid<T> grid;
+ private HandlerRegistration nativePreviewHandlerRegistration;
+
+ private final AutoScrollHandler autoScrollHandler = new AutoScrollHandler();
+
+ public MultiSelectionRenderer(final Grid<T> grid) {
+ this.grid = grid;
+ }
+
+ @Override
+ public void destroy() {
+ if (nativePreviewHandlerRegistration != null) {
+ removeNativeHandler();
+ }
+ }
+
+ @Override
+ public CheckBox createWidget() {
+ final CheckBox checkBox = GWT.create(CheckBox.class);
+ checkBox.setStylePrimaryName(
+ grid.getStylePrimaryName() + SELECTION_CHECKBOX_CLASSNAME);
+
+ CheckBoxEventHandler handler = new CheckBoxEventHandler(checkBox);
+
+ // Sink events
+ checkBox.sinkBitlessEvent(BrowserEvents.MOUSEDOWN);
+ checkBox.sinkBitlessEvent(BrowserEvents.TOUCHSTART);
+ checkBox.sinkBitlessEvent(BrowserEvents.CLICK);
+
+ // Add handlers
+ checkBox.addMouseDownHandler(handler);
+ checkBox.addTouchStartHandler(handler);
+ checkBox.addClickHandler(handler);
+ grid.addHandler(handler, GridEnabledEvent.TYPE);
+
+ checkBox.setEnabled(grid.isEnabled());
+
+ return checkBox;
+ }
+
+ @Override
+ public void render(final RendererCellReference cell, final Boolean data,
+ CheckBox checkBox) {
+ checkBox.setValue(data, false);
+ checkBox.setEnabled(grid.isEnabled() && !grid.isEditorActive());
+ checkBox.getElement().setPropertyInt(LOGICAL_ROW_PROPERTY_INT,
+ cell.getRowIndex());
+ }
+
+ @Override
+ public Collection<String> getConsumedEvents() {
+ final HashSet<String> events = new HashSet<String>();
+
+ /*
+ * this column's first interest is only to attach a NativePreventHandler
+ * that does all the magic. These events are the beginning of that
+ * cycle.
+ */
+ events.add(BrowserEvents.MOUSEDOWN);
+ events.add(BrowserEvents.TOUCHSTART);
+
+ return events;
+ }
+
+ @Override
+ public boolean onBrowserEvent(final CellReference<?> cell,
+ final NativeEvent event) {
+ if (BrowserEvents.TOUCHSTART.equals(event.getType())
+ || (BrowserEvents.MOUSEDOWN.equals(event.getType())
+ && event.getButton() == NativeEvent.BUTTON_LEFT)) {
+ startDragSelect(event, Element.as(event.getEventTarget()));
+ return true;
+ } else {
+ throw new IllegalStateException(
+ "received unexpected event: " + event.getType());
+ }
+ }
+
+ private void startDragSelect(NativeEvent event, final Element target) {
+ injectNativeHandler();
+ int logicalRowIndex = getLogicalRowIndex(target);
+ autoScrollHandler.start(logicalRowIndex);
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ private void injectNativeHandler() {
+ removeNativeHandler();
+ nativePreviewHandlerRegistration = Event
+ .addNativePreviewHandler(new TouchEventHandler());
+ }
+
+ private void removeNativeHandler() {
+ if (nativePreviewHandlerRegistration != null) {
+ nativePreviewHandlerRegistration.removeHandler();
+ nativePreviewHandlerRegistration = null;
+ }
+ }
+
+ private int getLogicalRowIndex(final Element target) {
+ if (target == null) {
+ return -1;
+ }
+
+ /*
+ * We can't simply go backwards until we find a <tr> first element,
+ * because of the table-in-table scenario. We need to, unfortunately, go
+ * up from our known root.
+ */
+ final Element tbody = getTbodyElement();
+ Element tr = tbody.getFirstChildElement();
+ while (tr != null) {
+ if (tr.isOrHasChild(target)) {
+ final Element td = tr.getFirstChildElement();
+ assert td != null : "Cell has disappeared";
+
+ final Element checkbox = td.getFirstChildElement();
+ assert checkbox != null : "Checkbox has disappeared";
+
+ return checkbox.getPropertyInt(LOGICAL_ROW_PROPERTY_INT);
+ }
+ tr = tr.getNextSiblingElement();
+ }
+ return -1;
+ }
+
+ private TableElement getTableElement() {
+ final Element root = grid.getElement();
+ final Element tablewrapper = Element.as(root.getChild(2));
+ if (tablewrapper != null) {
+ return TableElement.as(tablewrapper.getFirstChildElement());
+ } else {
+ return null;
+ }
+ }
+
+ private TableSectionElement getTbodyElement() {
+ TableElement table = getTableElement();
+ if (table != null) {
+ return table.getTBodies().getItem(0);
+ } else {
+ return null;
+ }
+ }
+
+ private TableSectionElement getTheadElement() {
+ TableElement table = getTableElement();
+ if (table != null) {
+ return table.getTHead();
+ } else {
+ return null;
+ }
+ }
+
+ private TableSectionElement getTfootElement() {
+ TableElement table = getTableElement();
+ if (table != null) {
+ return table.getTFoot();
+ } else {
+ return null;
+ }
+ }
+
+ /** Get the "top" of an element in relation to "client" coordinates. */
+ private int getClientTop(final Element e) {
+ return e.getAbsoluteTop();
+ }
+
+ private int getBodyClientBottom() {
+ return getClientTop(getTfootElement()) - 1;
+ }
+
+ private int getBodyClientTop() {
+ // Off by one pixel miscalculation. possibly border related.
+ return getClientTop(grid.getElement())
+ + getTheadElement().getOffsetHeight() + 1;
+ }
+
+ protected boolean isSelected(final int logicalRow) {
+ return grid.isSelected(grid.getDataSource().getRow(logicalRow));
+ }
+
+ protected void setSelected(final int logicalRow, final boolean select) {
+ T row = grid.getDataSource().getRow(logicalRow);
+ if (select) {
+ grid.select(row);
+ } else {
+ grid.deselect(row);
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionEvent.java
new file mode 100644
index 0000000000..0e6bda70a7
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionEvent.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Event object describing a change in Grid row selection state.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@SuppressWarnings("rawtypes")
+public class SelectionEvent<T> extends GwtEvent<SelectionHandler> {
+
+ private static final Type<SelectionHandler> eventType = new Type<SelectionHandler>();
+
+ private final Grid<T> grid;
+ private final List<T> added;
+ private final List<T> removed;
+ private final boolean batched;
+
+ /**
+ * Creates an event with a single added or removed row.
+ *
+ * @param grid
+ * grid reference, used for getSource
+ * @param added
+ * the added row, or <code>null</code> if a row was not added
+ * @param removed
+ * the removed row, or <code>null</code> if a row was not removed
+ * @param batched
+ * whether or not this selection change event is triggered during
+ * a batched selection/deselection action
+ * @see SelectionModel.Multi.Batched
+ */
+ public SelectionEvent(Grid<T> grid, T added, T removed, boolean batched) {
+ this.grid = grid;
+ this.batched = batched;
+
+ if (added != null) {
+ this.added = Collections.singletonList(added);
+ } else {
+ this.added = Collections.emptyList();
+ }
+
+ if (removed != null) {
+ this.removed = Collections.singletonList(removed);
+ } else {
+ this.removed = Collections.emptyList();
+ }
+ }
+
+ /**
+ * Creates an event where several rows have been added or removed.
+ *
+ * @param grid
+ * Grid reference, used for getSource
+ * @param added
+ * a collection of added rows, or <code>null</code> if no rows
+ * were added
+ * @param removed
+ * a collection of removed rows, or <code>null</code> if no rows
+ * were removed
+ * @param batched
+ * whether or not this selection change event is triggered during
+ * a batched selection/deselection action
+ * @see SelectionModel.Multi.Batched
+ */
+ public SelectionEvent(Grid<T> grid, Collection<T> added,
+ Collection<T> removed, boolean batched) {
+ this.grid = grid;
+ this.batched = batched;
+
+ if (added != null) {
+ this.added = new ArrayList<T>(added);
+ } else {
+ this.added = Collections.emptyList();
+ }
+
+ if (removed != null) {
+ this.removed = new ArrayList<T>(removed);
+ } else {
+ this.removed = Collections.emptyList();
+ }
+ }
+
+ /**
+ * Gets a reference to the Grid object that fired this event.
+ *
+ * @return a grid reference
+ */
+ @Override
+ public Grid<T> getSource() {
+ return grid;
+ }
+
+ /**
+ * Gets all rows added to the selection since the last
+ * {@link SelectionEvent} .
+ *
+ * @return a collection of added rows. Empty collection if no rows were
+ * added.
+ */
+ public Collection<T> getAdded() {
+ return Collections.unmodifiableCollection(added);
+ }
+
+ /**
+ * Gets all rows removed from the selection since the last
+ * {@link SelectionEvent}.
+ *
+ * @return a collection of removed rows. Empty collection if no rows were
+ * removed.
+ */
+ public Collection<T> getRemoved() {
+ return Collections.unmodifiableCollection(removed);
+ }
+
+ /**
+ * Gets currently selected rows.
+ *
+ * @return a non-null collection containing all currently selected rows.
+ */
+ public Collection<T> getSelected() {
+ return grid.getSelectedRows();
+ }
+
+ /**
+ * Gets a type identifier for this event.
+ *
+ * @return a {@link Type} identifier.
+ */
+ public static Type<SelectionHandler> getType() {
+ return eventType;
+ }
+
+ @Override
+ public Type<SelectionHandler> getAssociatedType() {
+ return eventType;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void dispatch(SelectionHandler handler) {
+ handler.onSelect(this);
+ }
+
+ /**
+ * Checks if this selection change event is fired during a batched
+ * selection/deselection operation.
+ *
+ * @return <code>true</code> iff this event is fired during a batched
+ * selection/deselection operation
+ */
+ public boolean isBatchedSelection() {
+ return batched;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionHandler.java
new file mode 100644
index 0000000000..0d56ae2b6c
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for {@link SelectionEvent}s.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * The row data type
+ * @since 7.4
+ */
+public interface SelectionHandler<T> extends EventHandler {
+
+ /**
+ * Called when a selection model's selection state is changed.
+ *
+ * @param event
+ * a selection event, containing info about rows that have been
+ * added to or removed from the selection.
+ */
+ public void onSelect(SelectionEvent<T> event);
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModel.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModel.java
new file mode 100644
index 0000000000..28e486242c
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModel.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import java.util.Collection;
+
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Common interface for all selection models.
+ * <p>
+ * Selection models perform tracking of selected rows in the Grid, as well as
+ * dispatching events when the selection state changes.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * Grid's row type
+ * @since 7.4
+ */
+public interface SelectionModel<T> {
+
+ /**
+ * Return true if the provided row is considered selected under the
+ * implementing selection model.
+ *
+ * @param row
+ * row object instance
+ * @return <code>true</code>, if the row given as argument is considered
+ * selected.
+ */
+ public boolean isSelected(T row);
+
+ /**
+ * Return the {@link Renderer} responsible for rendering the selection
+ * column.
+ *
+ * @return a renderer instance. If null is returned, a selection column will
+ * not be drawn.
+ */
+ public Renderer<Boolean> getSelectionColumnRenderer();
+
+ /**
+ * Tells this SelectionModel which Grid it belongs to.
+ * <p>
+ * Implementations are free to have this be a no-op. This method is called
+ * internally by Grid.
+ *
+ * @param grid
+ * a {@link Grid} instance; <code>null</code> when removing from
+ * Grid
+ */
+ public void setGrid(Grid<T> grid);
+
+ /**
+ * Resets the SelectionModel to the initial state.
+ * <p>
+ * This method can be called internally, for example, when the attached
+ * Grid's data source changes.
+ */
+ public void reset();
+
+ /**
+ * Returns a Collection containing all selected rows.
+ *
+ * @return a non-null collection.
+ */
+ public Collection<T> getSelectedRows();
+
+ /**
+ * Selection model that allows a maximum of one row to be selected at any
+ * one time.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Single<T> extends SelectionModel<T> {
+
+ /**
+ * Selects a row.
+ *
+ * @param row
+ * a {@link Grid} row object
+ * @return true, if this row as not previously selected.
+ */
+ public boolean select(T row);
+
+ /**
+ * Deselects a row.
+ * <p>
+ * This is a no-op unless {@link row} is the currently selected row.
+ *
+ * @param row
+ * a {@link Grid} row object
+ * @return true, if the currently selected row was deselected.
+ */
+ public boolean deselect(T row);
+
+ /**
+ * Returns the currently selected row.
+ *
+ * @return a {@link Grid} row object or null, if nothing is selected.
+ */
+ public T 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();
+
+ }
+
+ /**
+ * Selection model that allows for several rows to be selected at once.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Multi<T> extends SelectionModel<T> {
+
+ /**
+ * A multi selection model that can send selections and deselections in
+ * a batch, instead of committing them one-by-one.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Batched<T> extends Multi<T> {
+ /**
+ * Starts a batch selection.
+ * <p>
+ * Any commands to any select or deselect method will be batched
+ * into one, and a final selection event will be fired when
+ * {@link #commitBatchSelect()} is called.
+ * <p>
+ * <em>Note:</em> {@link SelectionEvent SelectionChangeEvents} will
+ * still be fired for each selection/deselection. You should check
+ * whether the event is a part of a batch or not with
+ * {@link SelectionEvent#isBatchedSelection()}.
+ */
+ public void startBatchSelect();
+
+ /**
+ * Commits and ends a batch selection.
+ * <p>
+ * Any and all selections and deselections since the last invocation
+ * of {@link #startBatchSelect()} will be fired at once as one
+ * collated {@link SelectionEvent}.
+ */
+ public void commitBatchSelect();
+
+ /**
+ * Checks whether or not a batch has been started.
+ *
+ * @return <code>true</code> iff a batch has been started
+ */
+ public boolean isBeingBatchSelected();
+
+ /**
+ * Gets all the rows that would become selected in this batch.
+ *
+ * @return a collection of the rows that would become selected
+ */
+ public Collection<T> getSelectedRowsBatch();
+
+ /**
+ * Gets all the rows that would become deselected in this batch.
+ *
+ * @return a collection of the rows that would become deselected
+ */
+ public Collection<T> getDeselectedRowsBatch();
+ }
+
+ /**
+ * Selects one or more rows.
+ *
+ * @param rows
+ * {@link Grid} row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean select(T... rows);
+
+ /**
+ * Deselects one or more rows.
+ *
+ * @param rows
+ * Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean deselect(T... rows);
+
+ /**
+ * De-selects all rows.
+ *
+ * @return true, if any row was previously selected.
+ */
+ public boolean deselectAll();
+
+ /**
+ * Select all rows in a {@link Collection}.
+ *
+ * @param rows
+ * a collection of Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean select(Collection<T> rows);
+
+ /**
+ * Deselect all rows in a {@link Collection}.
+ *
+ * @param rows
+ * a collection of Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean deselect(Collection<T> rows);
+
+ }
+
+ /**
+ * Interface for a selection model that does not allow anything to be
+ * selected.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface None<T> extends SelectionModel<T> {
+
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelMulti.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelMulti.java
new file mode 100644
index 0000000000..8629a5af3a
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelMulti.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Multi-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class SelectionModelMulti<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.Multi.Batched<T> {
+
+ private final LinkedHashSet<RowHandle<T>> selectedRows;
+ private Renderer<Boolean> renderer;
+ private Grid<T> grid;
+
+ private boolean batchStarted = false;
+ private final LinkedHashSet<RowHandle<T>> selectionBatch = new LinkedHashSet<RowHandle<T>>();
+ private final LinkedHashSet<RowHandle<T>> deselectionBatch = new LinkedHashSet<RowHandle<T>>();
+
+ /* Event handling for selection with space key */
+ private SpaceSelectHandler<T> spaceSelectHandler;
+
+ public SelectionModelMulti() {
+ grid = null;
+ renderer = null;
+ selectedRows = new LinkedHashSet<RowHandle<T>>();
+ }
+
+ @Override
+ public boolean isSelected(T row) {
+ return isSelectedByHandle(grid.getDataSource().getHandle(row));
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ return renderer;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ if (this.grid != null && grid != null) {
+ // Trying to replace grid
+ throw new IllegalStateException(
+ "Selection model is already attached to a grid. "
+ + "Remove the selection model first from "
+ + "the grid and then add it.");
+ }
+
+ this.grid = grid;
+ if (this.grid != null) {
+ spaceSelectHandler = new SpaceSelectHandler<T>(grid);
+ this.renderer = new MultiSelectionRenderer<T>(grid);
+ } else {
+ spaceSelectHandler.removeHandler();
+ spaceSelectHandler = null;
+ this.renderer = null;
+ }
+
+ }
+
+ @Override
+ public boolean select(T... rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+ return select(Arrays.asList(rows));
+ }
+
+ @Override
+ public boolean deselect(T... rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+ return deselect(Arrays.asList(rows));
+ }
+
+ @Override
+ public boolean deselectAll() {
+ if (selectedRows.size() > 0) {
+
+ @SuppressWarnings("unchecked")
+ final LinkedHashSet<RowHandle<T>> selectedRowsClone = (LinkedHashSet<RowHandle<T>>) selectedRows
+ .clone();
+ SelectionEvent<T> event = new SelectionEvent<T>(grid, null,
+ getSelectedRows(), isBeingBatchSelected());
+ selectedRows.clear();
+
+ if (isBeingBatchSelected()) {
+ selectionBatch.clear();
+ deselectionBatch.clear();
+ deselectionBatch.addAll(selectedRowsClone);
+ }
+
+ grid.fireEvent(event);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean select(Collection<T> rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+
+ Set<T> added = new LinkedHashSet<T>();
+
+ for (T row : rows) {
+ RowHandle<T> handle = grid.getDataSource().getHandle(row);
+ if (selectByHandle(handle)) {
+ added.add(row);
+ }
+ }
+
+ if (added.size() > 0) {
+ grid.fireEvent(new SelectionEvent<T>(grid, added, null,
+ isBeingBatchSelected()));
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deselect(Collection<T> rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+
+ Set<T> removed = new LinkedHashSet<T>();
+
+ for (T row : rows) {
+ RowHandle<T> handle = grid.getDataSource().getHandle(row);
+ if (deselectByHandle(handle)) {
+ removed.add(row);
+ }
+ }
+
+ if (removed.size() > 0) {
+ grid.fireEvent(new SelectionEvent<T>(grid, null, removed,
+ isBeingBatchSelected()));
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean isSelectedByHandle(RowHandle<T> handle) {
+ return selectedRows.contains(handle);
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle) {
+ if (selectedRows.add(handle)) {
+ handle.pin();
+
+ if (isBeingBatchSelected()) {
+ deselectionBatch.remove(handle);
+ selectionBatch.add(handle);
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle) {
+ if (selectedRows.remove(handle)) {
+
+ if (!isBeingBatchSelected()) {
+ handle.unpin();
+ } else {
+ selectionBatch.remove(handle);
+ deselectionBatch.add(handle);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ Set<T> selected = new LinkedHashSet<T>();
+ for (RowHandle<T> handle : selectedRows) {
+ selected.add(handle.getRow());
+ }
+ return Collections.unmodifiableSet(selected);
+ }
+
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+
+ @Override
+ public void startBatchSelect() {
+ assert !isBeingBatchSelected() : "Batch has already been started";
+ batchStarted = true;
+ }
+
+ @Override
+ public void commitBatchSelect() {
+ assert isBeingBatchSelected() : "Batch was never started";
+ if (!isBeingBatchSelected()) {
+ return;
+ }
+
+ batchStarted = false;
+
+ final Collection<T> added = getSelectedRowsBatch();
+ selectionBatch.clear();
+
+ final Collection<T> removed = getDeselectedRowsBatch();
+
+ // unpin deselected rows
+ for (RowHandle<T> handle : deselectionBatch) {
+ handle.unpin();
+ }
+ deselectionBatch.clear();
+
+ grid.fireEvent(new SelectionEvent<T>(grid, added, removed,
+ isBeingBatchSelected()));
+ }
+
+ @Override
+ public boolean isBeingBatchSelected() {
+ return batchStarted;
+ }
+
+ @Override
+ public Collection<T> getSelectedRowsBatch() {
+ return rowHandlesToRows(selectionBatch);
+ }
+
+ @Override
+ public Collection<T> getDeselectedRowsBatch() {
+ return rowHandlesToRows(deselectionBatch);
+ }
+
+ private ArrayList<T> rowHandlesToRows(Collection<RowHandle<T>> rowHandles) {
+ ArrayList<T> rows = new ArrayList<T>(rowHandles.size());
+ for (RowHandle<T> handle : rowHandles) {
+ rows.add(handle.getRow());
+ }
+ return rows;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelNone.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelNone.java
new file mode 100644
index 0000000000..eb208d9d49
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelNone.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * No-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class SelectionModelNone<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.None<T> {
+
+ @Override
+ public boolean isSelected(T row) {
+ return false;
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ return null;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ // noop
+ }
+
+ @Override
+ public void reset() {
+ // noop
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "This selection model " + "does not support selection");
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "This selection model " + "does not support deselection");
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelSingle.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelSingle.java
new file mode 100644
index 0000000000..6467759d6e
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SelectionModelSingle.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Single-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class SelectionModelSingle<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.Single<T> {
+
+ private Grid<T> grid;
+ private RowHandle<T> selectedRow;
+
+ /** Event handling for selection with space key */
+ private SpaceSelectHandler<T> spaceSelectHandler;
+
+ /** Event handling for selection by clicking cells */
+ private ClickSelectHandler<T> clickSelectHandler;
+
+ private boolean deselectAllowed = true;
+
+ @Override
+ public boolean isSelected(T row) {
+ return selectedRow != null
+ && selectedRow.equals(grid.getDataSource().getHandle(row));
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ // No Selection column renderer for single selection
+ return null;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ if (this.grid != null && grid != null) {
+ // Trying to replace grid
+ throw new IllegalStateException(
+ "Selection model is already attached to a grid. "
+ + "Remove the selection model first from "
+ + "the grid and then add it.");
+ }
+
+ this.grid = grid;
+ if (this.grid != null) {
+ spaceSelectHandler = new SpaceSelectHandler<T>(grid);
+ clickSelectHandler = new ClickSelectHandler<T>(grid);
+ updateHandlerDeselectAllowed();
+ } else {
+ spaceSelectHandler.removeHandler();
+ clickSelectHandler.removeHandler();
+ spaceSelectHandler = null;
+ clickSelectHandler = null;
+ }
+ }
+
+ @Override
+ public boolean select(T row) {
+
+ if (row == null) {
+ throw new IllegalArgumentException("Row cannot be null");
+ }
+
+ T removed = getSelectedRow();
+ if (selectByHandle(grid.getDataSource().getHandle(row))) {
+ grid.fireEvent(new SelectionEvent<T>(grid, row, removed, false));
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deselect(T row) {
+
+ if (row == null) {
+ throw new IllegalArgumentException("Row cannot be null");
+ }
+
+ if (isSelected(row)) {
+ deselectByHandle(selectedRow);
+ grid.fireEvent(new SelectionEvent<T>(grid, null, row, false));
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public T getSelectedRow() {
+ return (selectedRow != null ? selectedRow.getRow() : null);
+ }
+
+ @Override
+ public void reset() {
+ if (selectedRow != null) {
+ deselect(getSelectedRow());
+ }
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ if (getSelectedRow() != null) {
+ return Collections.singleton(getSelectedRow());
+ }
+ return Collections.emptySet();
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle) {
+ if (handle != null && !handle.equals(selectedRow)) {
+ deselectByHandle(selectedRow);
+ selectedRow = handle;
+ selectedRow.pin();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle) {
+ if (handle != null && handle.equals(selectedRow)) {
+ selectedRow.unpin();
+ selectedRow = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void setDeselectAllowed(boolean deselectAllowed) {
+ this.deselectAllowed = deselectAllowed;
+ updateHandlerDeselectAllowed();
+ }
+
+ @Override
+ public boolean isDeselectAllowed() {
+ return deselectAllowed;
+ }
+
+ private void updateHandlerDeselectAllowed() {
+ if (spaceSelectHandler != null) {
+ spaceSelectHandler.setDeselectAllowed(deselectAllowed);
+ }
+ if (clickSelectHandler != null) {
+ clickSelectHandler.setDeselectAllowed(deselectAllowed);
+ }
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SpaceSelectHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SpaceSelectHandler.java
new file mode 100644
index 0000000000..a3b05ff887
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/selection/SpaceSelectHandler.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.selection;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.v7.client.widget.grid.DataAvailableEvent;
+import com.vaadin.v7.client.widget.grid.DataAvailableHandler;
+import com.vaadin.v7.client.widget.grid.events.BodyKeyDownHandler;
+import com.vaadin.v7.client.widget.grid.events.BodyKeyUpHandler;
+import com.vaadin.v7.client.widget.grid.events.GridKeyDownEvent;
+import com.vaadin.v7.client.widget.grid.events.GridKeyUpEvent;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Generic class to perform selections when pressing space key.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * row data type
+ * @since 7.4
+ */
+public class SpaceSelectHandler<T> {
+
+ /**
+ * Handler for space key down events in Grid Body
+ */
+ private class SpaceKeyDownHandler implements BodyKeyDownHandler {
+ private HandlerRegistration scrollHandler = null;
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent event) {
+ if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE || spaceDown) {
+ return;
+ }
+
+ // Prevent space page scrolling
+ event.getNativeEvent().preventDefault();
+
+ spaceDown = true;
+ final int rowIndex = event.getFocusedCell().getRowIndex();
+
+ if (scrollHandler != null) {
+ scrollHandler.removeHandler();
+ scrollHandler = null;
+ }
+
+ scrollHandler = grid
+ .addDataAvailableHandler(new DataAvailableHandler() {
+
+ @Override
+ public void onDataAvailable(
+ DataAvailableEvent dataAvailableEvent) {
+ if (dataAvailableEvent.getAvailableRows()
+ .contains(rowIndex)) {
+ setSelected(grid, rowIndex);
+ scrollHandler.removeHandler();
+ scrollHandler = null;
+ }
+ }
+ });
+ grid.scrollToRow(rowIndex, ScrollDestination.ANY);
+ }
+
+ protected void setSelected(Grid<T> grid, int rowIndex) {
+ T row = grid.getDataSource().getRow(rowIndex);
+
+ if (!grid.isSelected(row)) {
+ grid.select(row);
+ } else if (deselectAllowed) {
+ grid.deselect(row);
+ }
+ }
+ }
+
+ private boolean spaceDown = false;
+ private Grid<T> grid;
+ private HandlerRegistration spaceUpHandler;
+ private HandlerRegistration spaceDownHandler;
+ private boolean deselectAllowed = true;
+
+ /**
+ * Constructor for SpaceSelectHandler. This constructor will add all
+ * necessary handlers for selecting rows with space.
+ *
+ * @param grid
+ * grid to attach to
+ */
+ public SpaceSelectHandler(Grid<T> grid) {
+ this.grid = grid;
+ spaceDownHandler = grid
+ .addBodyKeyDownHandler(new SpaceKeyDownHandler());
+ spaceUpHandler = grid.addBodyKeyUpHandler(new BodyKeyUpHandler() {
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_SPACE) {
+ spaceDown = false;
+ }
+ }
+ });
+ }
+
+ /**
+ * Clean up function for removing all now obsolete handlers.
+ */
+ public void removeHandler() {
+ spaceDownHandler.removeHandler();
+ spaceUpHandler.removeHandler();
+ }
+
+ /**
+ * Sets whether pressing space for the currently selected row should
+ * deselect the row.
+ *
+ * @param deselectAllowed
+ * <code>true</code> to allow deselecting the selected row;
+ * otherwise <code>false</code>
+ */
+ public void setDeselectAllowed(boolean deselectAllowed) {
+ this.deselectAllowed = deselectAllowed;
+ }
+} \ No newline at end of file
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/Sort.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/Sort.java
new file mode 100644
index 0000000000..c7fe0457e8
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/Sort.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.sort;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Fluid Sort descriptor object.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Sort {
+
+ private final Sort previous;
+ private final SortOrder order;
+ private final int count;
+
+ /**
+ * Basic constructor, used by the {@link #by(GridColumn)} and
+ * {@link #by(GridColumn, SortDirection)} methods.
+ *
+ * @param column
+ * a grid column
+ * @param direction
+ * a sort direction
+ */
+ private Sort(Grid.Column<?, ?> column, SortDirection direction) {
+ previous = null;
+ count = 1;
+ order = new SortOrder(column, direction);
+ }
+
+ /**
+ * Extension constructor. Performs object equality checks on all previous
+ * Sort objects in the chain to make sure that the column being passed in
+ * isn't already used earlier (which would indicate a bug). If the column
+ * has been used before, this constructor throws an
+ * {@link IllegalStateException}.
+ *
+ * @param previous
+ * the sort instance that the new sort instance is to extend
+ * @param column
+ * a (previously unused) grid column reference
+ * @param direction
+ * a sort direction
+ */
+ private Sort(Sort previous, Grid.Column<?, ?> column,
+ SortDirection direction) {
+ this.previous = previous;
+ count = previous.count + 1;
+ order = new SortOrder(column, direction);
+
+ Sort s = previous;
+ while (s != null) {
+ if (s.order.getColumn() == column) {
+ throw new IllegalStateException(
+ "Can not sort along the same column twice");
+ }
+ s = s.previous;
+ }
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column in ascending
+ * order.
+ *
+ * @param column
+ * a grid column object reference
+ * @return a sort instance, typed to the grid data type
+ */
+ public static Sort by(Grid.Column<?, ?> column) {
+ return by(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column.
+ *
+ * @param column
+ * a grid column object reference
+ * @param direction
+ * indicator of sort direction - either ascending or descending
+ * @return a sort instance, typed to the grid data type
+ */
+ public static Sort by(Grid.Column<?, ?> column, SortDirection direction) {
+ return new Sort(column, direction);
+ }
+
+ /**
+ * Continue building a Sort order. The provided column is sorted in
+ * ascending order if the previously added columns have been evaluated as
+ * equals.
+ *
+ * @param column
+ * a grid column object reference
+ * @return a sort instance, typed to the grid data type
+ */
+ public Sort then(Grid.Column<?, ?> column) {
+ return then(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Continue building a Sort order. The provided column is sorted in
+ * specified order if the previously added columns have been evaluated as
+ * equals.
+ *
+ * @param column
+ * a grid column object reference
+ * @param direction
+ * indicator of sort direction - either ascending or descending
+ * @return a sort instance, typed to the grid data type
+ */
+ public Sort then(Grid.Column<?, ?> column, SortDirection direction) {
+ return new Sort(this, column, direction);
+ }
+
+ /**
+ * Build a sort order list. This method is called internally by Grid when
+ * calling {@link com.vaadin.client.ui.grid.Grid#sort(Sort)}, but can also
+ * be called manually to create a SortOrder list, which can also be provided
+ * directly to Grid.
+ *
+ * @return a sort order list.
+ */
+ public List<SortOrder> build() {
+
+ List<SortOrder> order = new ArrayList<SortOrder>(count);
+
+ Sort s = this;
+ for (int i = count - 1; i >= 0; --i) {
+ order.add(0, s.order);
+ s = s.previous;
+ }
+
+ return order;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortEvent.java
new file mode 100644
index 0000000000..297e898c08
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortEvent.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.sort;
+
+import java.util.List;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * A sort event, fired by the Grid when it needs its data source to provide data
+ * sorted in a specific manner.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SortEvent<T> extends GwtEvent<SortHandler<?>> {
+
+ private static final Type<SortHandler<?>> TYPE = new Type<SortHandler<?>>();
+
+ private final Grid<T> grid;
+ private final List<SortOrder> order;
+ private final boolean userOriginated;
+
+ /**
+ * Creates a new Sort Event. All provided parameters are final, and passed
+ * on as-is.
+ *
+ * @param grid
+ * a grid reference
+ * @param order
+ * an array dictating the desired sort order of the data source
+ * @param originator
+ * a value indicating where this event originated from
+ */
+ public SortEvent(Grid<T> grid, List<SortOrder> order,
+ boolean userOriginated) {
+ this.grid = grid;
+ this.order = order;
+ this.userOriginated = userOriginated;
+ }
+
+ @Override
+ public Type<SortHandler<?>> getAssociatedType() {
+ return TYPE;
+ }
+
+ /**
+ * Static access to the GWT event type identifier associated with this Event
+ * class
+ *
+ * @return a type object, uniquely describing this event type.
+ */
+ public static Type<SortHandler<?>> getType() {
+ return TYPE;
+ }
+
+ /**
+ * Get access to the Grid that fired this event
+ *
+ * @return the grid instance
+ */
+ @Override
+ public Grid<T> getSource() {
+ return grid;
+ }
+
+ /**
+ * Get access to the Grid that fired this event
+ *
+ * @return the grid instance
+ */
+ public Grid<T> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Get the sort ordering that is to be applied to the Grid
+ *
+ * @return a list of sort order objects
+ */
+ public List<SortOrder> getOrder() {
+ return order;
+ }
+
+ /**
+ * Returns whether this event originated from actions done by the user.
+ *
+ * @return true if sort event originated from user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void dispatch(SortHandler<?> handler) {
+ ((SortHandler<T>) handler).sort(this);
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortHandler.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortHandler.java
new file mode 100644
index 0000000000..0b72178599
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.sort;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid sort event, called when the Grid needs its data source to
+ * provide data sorted in a specific manner.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface SortHandler<T> extends EventHandler {
+
+ /**
+ * Handle sorting of the Grid. This method is called when a re-sorting of
+ * the Grid's data is requested.
+ *
+ * @param event
+ * the sort event
+ */
+ public void sort(SortEvent<T> event);
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortOrder.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortOrder.java
new file mode 100644
index 0000000000..4e2f634d20
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/sort/SortOrder.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2016 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.v7.client.widget.grid.sort;
+
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.v7.client.widgets.Grid;
+
+/**
+ * Sort order descriptor. Contains column and direction references.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SortOrder {
+
+ private final Grid.Column<?, ?> column;
+ private final SortDirection direction;
+
+ /**
+ * Create a sort order descriptor with a default sorting direction value of
+ * {@link SortDirection#ASCENDING}.
+ *
+ * @param column
+ * a grid column descriptor object
+ */
+ public SortOrder(Grid.Column<?, ?> column) {
+ this(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Create a sort order descriptor.
+ *
+ * @param column
+ * a grid column descriptor object
+ * @param direction
+ * a sorting direction value (ascending or descending)
+ */
+ public SortOrder(Grid.Column<?, ?> column, SortDirection direction) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Grid column reference can not be null!");
+ }
+ if (direction == null) {
+ throw new IllegalArgumentException(
+ "Direction value can not be null!");
+ }
+ this.column = column;
+ this.direction = direction;
+ }
+
+ /**
+ * Returns the {@link GridColumn} reference given in the constructor.
+ *
+ * @return a grid column reference
+ */
+ public Grid.Column<?, ?> getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the {@link SortDirection} value given in the constructor.
+ *
+ * @return a sort direction value
+ */
+ public SortDirection getDirection() {
+ return direction;
+ }
+
+ /**
+ * Returns a new SortOrder object with the sort direction reversed.
+ *
+ * @return a new sort order object
+ */
+ public SortOrder getOpposite() {
+ return new SortOrder(column, direction.getOpposite());
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Escalator.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Escalator.java
new file mode 100644
index 0000000000..322a2e54c4
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Escalator.java
@@ -0,0 +1,6785 @@
+/*
+ * Copyright 2000-2016 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.v7.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.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.v7.client.widget.escalator.Cell;
+import com.vaadin.v7.client.widget.escalator.ColumnConfiguration;
+import com.vaadin.v7.client.widget.escalator.EscalatorUpdater;
+import com.vaadin.v7.client.widget.escalator.FlyweightCell;
+import com.vaadin.v7.client.widget.escalator.FlyweightRow;
+import com.vaadin.v7.client.widget.escalator.PositionFunction;
+import com.vaadin.v7.client.widget.escalator.PositionFunction.AbsolutePosition;
+import com.vaadin.v7.client.widget.escalator.PositionFunction.Translate3DPosition;
+import com.vaadin.v7.client.widget.escalator.PositionFunction.TranslatePosition;
+import com.vaadin.v7.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition;
+import com.vaadin.v7.client.widget.escalator.Row;
+import com.vaadin.v7.client.widget.escalator.RowContainer;
+import com.vaadin.v7.client.widget.escalator.RowContainer.BodyRowContainer;
+import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeEvent;
+import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeHandler;
+import com.vaadin.v7.client.widget.escalator.ScrollbarBundle;
+import com.vaadin.v7.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle;
+import com.vaadin.v7.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle;
+import com.vaadin.v7.client.widget.escalator.Spacer;
+import com.vaadin.v7.client.widget.escalator.SpacerUpdater;
+import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedEvent;
+import com.vaadin.v7.client.widget.grid.events.ScrollEvent;
+import com.vaadin.v7.client.widget.grid.events.ScrollHandler;
+import com.vaadin.v7.client.widgets.Escalator.JsniUtil.TouchHandlerBundle;
+
+/*-
+
+ 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.v7.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.v7.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.v7.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.v7.client.widgets.Escalator::verticalScrollbar;
+ var vScrollElem = vScroll.@com.vaadin.v7.client.widget.escalator.ScrollbarBundle::getElement()();
+
+ var hScroll = esc.@com.vaadin.v7.client.widgets.Escalator::horizontalScrollbar;
+ var hScrollElem = hScroll.@com.vaadin.v7.client.widget.escalator.ScrollbarBundle::getElement()();
+
+ return $entry(function(e) {
+ var target = e.target;
+
+ // 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.v7.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
+ } else if (target === hScrollElem) {
+ hScroll.@com.vaadin.v7.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.v7.client.widgets.Escalator::body;
+ deltaY *= brc.@com.vaadin.v7.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.v7.client.widgets.Escalator::logWarning(*)(msg);
+ }
+
+ // IE8 has only delta y
+ if (isNaN(deltaY)) {
+ deltaY = -0.5*e.wheelDelta;
+ }
+
+ @com.vaadin.v7.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.v7.client.widgets.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.attachEvent("onscroll", this.@com.vaadin.v7.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.v7.client.widgets.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.detachEvent("onscroll", this.@com.vaadin.v7.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.
+ */
+ /*-{
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel';
+ element.addEventListener(eventName, this.@com.vaadin.v7.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.
+ */
+ /*-{
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = element.onwheel===undefined?"mousewheel":"wheel";
+ element.removeEventListener(eventName, this.@com.vaadin.v7.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.
+ */
+ /*-{
+ element.addEventListener("touchstart", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchStartFunction);
+ element.addEventListener("touchmove", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchMoveFunction);
+ element.addEventListener("touchend", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction);
+ element.addEventListener("touchcancel", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction);
+ }-*/;
+
+ 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.
+ */
+ /*-{
+ element.removeEventListener("touchstart", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchStartFunction);
+ element.removeEventListener("touchmove", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchMoveFunction);
+ element.removeEventListener("touchend", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction);
+ element.removeEventListener("touchcancel", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction);
+ }-*/;
+
+ 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 (heightMode == HeightMode.UNDEFINED) {
+ heightByRows = rows;
+ }
+
+ 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;
+ if (heightMode == HeightMode.UNDEFINED) {
+ heightByRows = rows;
+ }
+
+ /*
+ * 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;
+ }
+ }
+ });
+ }
+
+ private void fireRowHeightChangedEventFinally() {
+ if (!rowHeightChangedEventFired) {
+ rowHeightChangedEventFired = true;
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ fireEvent(new RowHeightChangedEvent());
+ rowHeightChangedEventFired = false;
+ }
+ });
+ }
+ }
+
+ public void autodetectRowHeightNow() {
+ if (!isAttached()) {
+ // Run again when attached
+ defaultRowHeightShouldBeAutodetected = true;
+ return;
+ }
+
+ final double oldRowHeight = defaultRowHeight;
+
+ 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();
+ }
+
+ if (oldRowHeight != defaultRowHeight) {
+ fireRowHeightChangedEventFinally();
+ }
+ }
+
+ @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) {
+ assertArgumentsAreValidAndWithinRange(logicalRowRange.getStart(),
+ logicalRowRange.length());
+
+ if (!isAttached()) {
+ return;
+ }
+
+ Profiler.enter("Escalator.AbstractStaticRowContainer.refreshCells");
+
+ if (hasColumnAndRowData()) {
+ for (int row = logicalRowRange.getStart(); row < logicalRowRange
+ .getEnd(); row++) {
+ final TableRowElement tr = getTrByVisualIndex(row);
+ refreshRow(tr, row, colRange);
+ }
+ }
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.refreshCells");
+ }
+
+ @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]) &rarr; [0..9]
+ * <li>convertToVisual([15..24]) &rarr; [0..4]
+ * <li>convertToVisual([25..29]) &rarr; [5..9]
+ * <li>convertToVisual([26..39]) &rarr; [6..9]
+ * <li>convertToVisual([0..5]) &rarr; [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([35..1]) &rarr; [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([0..100]) &rarr; [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>)|&times;(180/&pi;)&nbsp;=&nbsp;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>)|&times;(180/&pi;)&nbsp;=&nbsp;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);
+
+ /**
+ * Flag for keeping track of {@link RowHeightChangedEvent}s
+ */
+ private boolean rowHeightChangedEventFired = false;
+
+ 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.
+ */
+ scrollbarThickness += 1;
+ }
+
+ root.appendChild(verticalScrollbar.getElement());
+ verticalScrollbar.addScrollHandler(scrollHandler);
+ verticalScrollbar.setScrollbarThickness(scrollbarThickness);
+
+ 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 {
+ if (getHeightMode() == HeightMode.UNDEFINED) {
+ heightByRows = body.getRowCount();
+ applyHeightByRows();
+ return;
+ } 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 {
+ if (getHeightMode() == HeightMode.UNDEFINED) {
+ int newHeightByRows = body.getRowCount();
+ if (heightByRows != newHeightByRows) {
+ heightByRows = newHeightByRows;
+ applyHeightByRows();
+ }
+ return;
+ } 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 &leq; 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
+ && heightMode != HeightMode.UNDEFINED) {
+ return;
+ }
+
+ double headerHeight = header.getHeightOfSection();
+ double footerHeight = footer.getHeightOfSection();
+ double bodyHeight = body.getDefaultRowHeight() * heightByRows;
+ double scrollbar = horizontalScrollbar.showsScrollHandle()
+ ? horizontalScrollbar.getScrollbarThickness() : 0;
+ double spacerHeight = 0; // ignored if HeightMode.ROW
+ if (heightMode == HeightMode.UNDEFINED) {
+ spacerHeight = body.spacerContainer.getSpacerHeightsSum();
+ }
+
+ double totalHeight = headerHeight + bodyHeight + spacerHeight
+ + 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;
+ case UNDEFINED:
+ setHeightByRows(body.getRowCount());
+ 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);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Grid.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Grid.java
new file mode 100644
index 0000000000..e6eb36f8c9
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Grid.java
@@ -0,0 +1,8968 @@
+/*
+ * Copyright 2000-2016 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.v7.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.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.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.widgets.Overlay;
+import com.vaadin.shared.Registration;
+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;
+import com.vaadin.v7.client.renderers.ComplexRenderer;
+import com.vaadin.v7.client.renderers.Renderer;
+import com.vaadin.v7.client.renderers.WidgetRenderer;
+import com.vaadin.v7.client.widget.escalator.Cell;
+import com.vaadin.v7.client.widget.escalator.ColumnConfiguration;
+import com.vaadin.v7.client.widget.escalator.EscalatorUpdater;
+import com.vaadin.v7.client.widget.escalator.FlyweightCell;
+import com.vaadin.v7.client.widget.escalator.Row;
+import com.vaadin.v7.client.widget.escalator.RowContainer;
+import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeEvent;
+import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeHandler;
+import com.vaadin.v7.client.widget.escalator.Spacer;
+import com.vaadin.v7.client.widget.escalator.SpacerUpdater;
+import com.vaadin.v7.client.widget.escalator.ScrollbarBundle.Direction;
+import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedEvent;
+import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedHandler;
+import com.vaadin.v7.client.widget.grid.AutoScroller;
+import com.vaadin.v7.client.widget.grid.CellReference;
+import com.vaadin.v7.client.widget.grid.CellStyleGenerator;
+import com.vaadin.v7.client.widget.grid.DataAvailableEvent;
+import com.vaadin.v7.client.widget.grid.DataAvailableHandler;
+import com.vaadin.v7.client.widget.grid.DefaultEditorEventHandler;
+import com.vaadin.v7.client.widget.grid.DetailsGenerator;
+import com.vaadin.v7.client.widget.grid.EditorHandler;
+import com.vaadin.v7.client.widget.grid.EventCellReference;
+import com.vaadin.v7.client.widget.grid.HeightAwareDetailsGenerator;
+import com.vaadin.v7.client.widget.grid.RendererCellReference;
+import com.vaadin.v7.client.widget.grid.RowReference;
+import com.vaadin.v7.client.widget.grid.RowStyleGenerator;
+import com.vaadin.v7.client.widget.grid.AutoScroller.AutoScrollerCallback;
+import com.vaadin.v7.client.widget.grid.AutoScroller.ScrollAxis;
+import com.vaadin.v7.client.widget.grid.EditorHandler.EditorRequest;
+import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler;
+import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler;
+import com.vaadin.v7.client.widget.grid.events.BodyClickHandler;
+import com.vaadin.v7.client.widget.grid.events.BodyDoubleClickHandler;
+import com.vaadin.v7.client.widget.grid.events.BodyKeyDownHandler;
+import com.vaadin.v7.client.widget.grid.events.BodyKeyPressHandler;
+import com.vaadin.v7.client.widget.grid.events.BodyKeyUpHandler;
+import com.vaadin.v7.client.widget.grid.events.ColumnReorderEvent;
+import com.vaadin.v7.client.widget.grid.events.ColumnReorderHandler;
+import com.vaadin.v7.client.widget.grid.events.ColumnResizeEvent;
+import com.vaadin.v7.client.widget.grid.events.ColumnResizeHandler;
+import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeEvent;
+import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeHandler;
+import com.vaadin.v7.client.widget.grid.events.FooterClickHandler;
+import com.vaadin.v7.client.widget.grid.events.FooterDoubleClickHandler;
+import com.vaadin.v7.client.widget.grid.events.FooterKeyDownHandler;
+import com.vaadin.v7.client.widget.grid.events.FooterKeyPressHandler;
+import com.vaadin.v7.client.widget.grid.events.FooterKeyUpHandler;
+import com.vaadin.v7.client.widget.grid.events.GridClickEvent;
+import com.vaadin.v7.client.widget.grid.events.GridDoubleClickEvent;
+import com.vaadin.v7.client.widget.grid.events.GridEnabledEvent;
+import com.vaadin.v7.client.widget.grid.events.GridEnabledHandler;
+import com.vaadin.v7.client.widget.grid.events.GridKeyDownEvent;
+import com.vaadin.v7.client.widget.grid.events.GridKeyPressEvent;
+import com.vaadin.v7.client.widget.grid.events.GridKeyUpEvent;
+import com.vaadin.v7.client.widget.grid.events.HeaderClickHandler;
+import com.vaadin.v7.client.widget.grid.events.HeaderDoubleClickHandler;
+import com.vaadin.v7.client.widget.grid.events.HeaderKeyDownHandler;
+import com.vaadin.v7.client.widget.grid.events.HeaderKeyPressHandler;
+import com.vaadin.v7.client.widget.grid.events.HeaderKeyUpHandler;
+import com.vaadin.v7.client.widget.grid.events.ScrollEvent;
+import com.vaadin.v7.client.widget.grid.events.ScrollHandler;
+import com.vaadin.v7.client.widget.grid.events.SelectAllEvent;
+import com.vaadin.v7.client.widget.grid.events.SelectAllHandler;
+import com.vaadin.v7.client.widget.grid.selection.HasSelectionHandlers;
+import com.vaadin.v7.client.widget.grid.selection.MultiSelectionRenderer;
+import com.vaadin.v7.client.widget.grid.selection.SelectionEvent;
+import com.vaadin.v7.client.widget.grid.selection.SelectionHandler;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModelMulti;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModelNone;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModelSingle;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Multi;
+import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Single;
+import com.vaadin.v7.client.widget.grid.sort.Sort;
+import com.vaadin.v7.client.widget.grid.sort.SortEvent;
+import com.vaadin.v7.client.widget.grid.sort.SortHandler;
+import com.vaadin.v7.client.widget.grid.sort.SortOrder;
+import com.vaadin.v7.client.widgets.Escalator.AbstractRowContainer;
+import com.vaadin.v7.client.widgets.Escalator.SubPartArguments;
+import com.vaadin.v7.client.widgets.Grid.Editor.State;
+import com.vaadin.v7.client.widgets.Grid.StaticSection.StaticCell;
+import com.vaadin.v7.client.widgets.Grid.StaticSection.StaticRow;
+
+/**
+ * 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.v7.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.v7.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.v7.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";
+
+ /**
+ * 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) {
+ 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 (container == null || 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>
+ implements GridEnabledHandler {
+
+ private boolean initDone = false;
+ private boolean selected = false;
+ private CheckBox selectAllCheckBox;
+
+ SelectionColumn(final Renderer<Boolean> selectColumnRenderer) {
+ super(selectColumnRenderer);
+
+ addEnabledHandler(this);
+ }
+
+ 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;
+ }
+
+ /**
+ * Sets whether the selection column is enabled.
+ *
+ * @since 7.7
+ * @param enabled
+ * <code>true</code> to enable the column, <code>false</code>
+ * to disable it.
+ */
+ public void setEnabled(boolean enabled) {
+ if (selectAllCheckBox != null) {
+ selectAllCheckBox.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void onEnabled(boolean enabled) {
+ setEnabled(enabled);
+ }
+ }
+
+ /**
+ * 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.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);
+ if (getHeightMode() == HeightMode.UNDEFINED) {
+ setHeightByRows(getEscalator().getBody().getRowCount());
+ }
+ }
+
+ @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();
+ if (getHeightMode() == HeightMode.UNDEFINED) {
+ // update spacer height
+ escalator.getBody().setSpacer(spacer.getRow(), 0);
+ setHeightByRows(getEscalator().getBody().getRowCount());
+ }
+ }
+ }
+
+ 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");
+ }
+ });
+ overlay.setFitInWindow(true);
+ }
+
+ /**
+ * 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;
+ private Registration changeHandler;
+
+ /**
+ * 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 : getVisibleColumns()) {
+ 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);
+
+ fireEvent(new GridEnabledEvent(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 (changeHandler != null) {
+ changeHandler.remove();
+ changeHandler = null;
+ }
+
+ this.dataSource = dataSource;
+ changeHandler = dataSource
+ .addDataChangeHandler(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.setEnabled(isEnabled());
+ 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());
+ }
+
+ /**
+ * Register a enabled status change handler to this Grid. The event for this
+ * handler is fired when the Grid changes from disabled to enabled and
+ * vice-versa.
+ *
+ * @param handler
+ * the handler for the event
+ * @return the registration for the event
+ */
+ public HandlerRegistration addEnabledHandler(GridEnabledHandler handler) {
+ return addHandler(handler, GridEnabledEvent.TYPE);
+ }
+
+ public HandlerRegistration addRowHeightChangedHandler(
+ RowHeightChangedHandler handler) {
+ return escalator.addHandler(handler, RowHeightChangedEvent.TYPE);
+ }
+
+ /**
+ * 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;
+ }
+}