diff options
Diffstat (limited to 'compatibility-client')
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 <T> 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 < 0 or > 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<{@link Cell}></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 < 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 < + * {@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<Integer> ds = new ListDataSource<Integer>(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 < + * {@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]) → [0..9] + * <li>convertToVisual([15..24]) → [0..4] + * <li>convertToVisual([25..29]) → [5..9] + * <li>convertToVisual([26..39]) → [6..9] + * <li>convertToVisual([0..5]) → [0..-1] <em>(empty)</em> + * <li>convertToVisual([35..1]) → [0..-1] <em>(empty)</em> + * <li>convertToVisual([0..100]) → [0..9] + * </ul> + * + * @return a logical range converted to a visual range, truncated to the + * current viewport. The first visual row has the index 0. + */ + private Range convertToVisual(final Range logicalRange) { + + if (logicalRange.isEmpty()) { + return logicalRange; + } else if (visualRowOrder.isEmpty()) { + // empty range + return Range.withLength(0, 0); + } + + /* + * TODO [[spacer]]: these assumptions will be totally broken with + * spacers. + */ + final int maxEscalatorRows = getMaxEscalatorRowCapacity(); + final int currentTopRowIndex = getLogicalRowIndex( + visualRowOrder.getFirst()); + + final Range[] partitions = logicalRange.partitionWith( + Range.withLength(currentTopRowIndex, maxEscalatorRows)); + final Range insideRange = partitions[1]; + return insideRange.offsetBy(-currentTopRowIndex); + } + + @Override + protected String getCellElementTagName() { + return "td"; + } + + @Override + protected double getHeightOfSection() { + final int tableHeight = tableWrapper.getOffsetHeight(); + final double footerHeight = footer.getHeightOfSection(); + final double headerHeight = header.getHeightOfSection(); + + double heightOfSection = tableHeight - footerHeight - headerHeight; + return Math.max(0, heightOfSection); + } + + @Override + protected void refreshCells(Range logicalRowRange, Range colRange) { + Profiler.enter("Escalator.BodyRowContainer.refreshRows"); + + final Range visualRange = convertToVisual(logicalRowRange); + + if (!visualRange.isEmpty()) { + final int firstLogicalRowIndex = getLogicalRowIndex( + visualRowOrder.getFirst()); + for (int rowNumber = visualRange + .getStart(); rowNumber < visualRange + .getEnd(); rowNumber++) { + refreshRow(visualRowOrder.get(rowNumber), + firstLogicalRowIndex + rowNumber, colRange); + } + } + + Profiler.leave("Escalator.BodyRowContainer.refreshRows"); + } + + @Override + protected TableRowElement getTrByVisualIndex(final int index) + throws IndexOutOfBoundsException { + if (index >= 0 && index < visualRowOrder.size()) { + return visualRowOrder.get(index); + } else { + throw new IndexOutOfBoundsException( + "No such visual index: " + index); + } + } + + @Override + public TableRowElement getRowElement(int index) { + if (index < 0 || index >= getRowCount()) { + throw new IndexOutOfBoundsException( + "No such logical index: " + index); + } + int visualIndex = index + - getLogicalRowIndex(visualRowOrder.getFirst()); + if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) { + return super.getRowElement(visualIndex); + } else { + throw new IllegalStateException("Row with logical index " + + index + " is currently not available in the DOM"); + } + } + + private void setBodyScrollPosition(final double scrollLeft, + final double scrollTop) { + tBodyScrollLeft = scrollLeft; + tBodyScrollTop = scrollTop; + position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop); + position.set(spacerDecoContainer, 0, -tBodyScrollTop); + } + + /** + * Make sure that there is a correct amount of escalator rows: Add more + * if needed, or remove any superfluous ones. + * <p> + * This method should be called when e.g. the height of the Escalator + * changes. + * <p> + * <em>Note:</em> This method will make sure that the escalator rows are + * placed in the proper places. By default new rows are added below, but + * if the content is scrolled down, the rows are populated on top + * instead. + */ + public void verifyEscalatorCount() { + /* + * This method indeed has a smell very similar to paintRemoveRows + * and paintInsertRows. + * + * Unfortunately, those the code can't trivially be shared, since + * there are some slight differences in the respective + * responsibilities. The "paint" methods fake the addition and + * removal of rows, and make sure to either push existing data out + * of view, or draw new data into view. Only in some special cases + * will the DOM element count change. + * + * This method, however, has the explicit responsibility to verify + * that when "something" happens, we still have the correct amount + * of escalator rows in the DOM, and if not, we make sure to modify + * that count. Only in some special cases do we need to take into + * account other things than simply modifying the DOM element count. + */ + + Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount"); + + if (!isAttached()) { + return; + } + + final int maxEscalatorRows = getMaxEscalatorRowCapacity(); + final int neededEscalatorRows = Math.min(maxEscalatorRows, + body.getRowCount()); + final int neededEscalatorRowsDiff = neededEscalatorRows + - visualRowOrder.size(); + + if (neededEscalatorRowsDiff > 0) { + // needs more + + /* + * This is a workaround for the issue where we might be scrolled + * to the bottom, and the widget expands beyond the content + * range + */ + + final int index = visualRowOrder.size(); + final int nextLastLogicalIndex; + if (!visualRowOrder.isEmpty()) { + nextLastLogicalIndex = getLogicalRowIndex( + visualRowOrder.getLast()) + 1; + } else { + nextLastLogicalIndex = 0; + } + + final boolean contentWillFit = nextLastLogicalIndex < getRowCount() + - neededEscalatorRowsDiff; + if (contentWillFit) { + final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded( + index, neededEscalatorRowsDiff); + + /* + * Since fillAndPopulateEscalatorRowsIfNeeded operates on + * the assumption that index == visual index == logical + * index, we thank for the added escalator rows, but since + * they're painted in the wrong CSS position, we need to + * move them to their actual locations. + * + * Note: this is the second (see body.paintInsertRows) + * occasion where fillAndPopulateEscalatorRowsIfNeeded would + * behave "more correctly" if it only would add escalator + * rows to the DOM and appropriate bookkeping, and not + * actually populate them :/ + */ + moveAndUpdateEscalatorRows( + Range.withLength(index, addedRows.size()), index, + nextLastLogicalIndex); + } else { + /* + * TODO [[optimize]] + * + * We're scrolled so far down that all rows can't be simply + * appended at the end, since we might start displaying + * escalator rows that don't exist. To avoid the mess that + * is body.paintRemoveRows, this is a dirty hack that dumbs + * the problem down to a more basic and already-solved + * problem: + * + * 1) scroll all the way up 2) add the missing escalator + * rows 3) scroll back to the original position. + * + * Letting the browser scroll back to our original position + * will automatically solve any possible overflow problems, + * since the browser will not allow us to scroll beyond the + * actual content. + */ + + final double oldScrollTop = getScrollTop(); + setScrollTop(0); + scroller.onScroll(); + fillAndPopulateEscalatorRowsIfNeeded(index, + neededEscalatorRowsDiff); + setScrollTop(oldScrollTop); + scroller.onScroll(); + } + } + + else if (neededEscalatorRowsDiff < 0) { + // needs less + + final ListIterator<TableRowElement> iter = visualRowOrder + .listIterator(visualRowOrder.size()); + for (int i = 0; i < -neededEscalatorRowsDiff; i++) { + final Element last = iter.previous(); + last.removeFromParent(); + iter.remove(); + } + + /* + * If we were scrolled to the bottom so that we didn't have an + * extra escalator row at the bottom, we'll probably end up with + * blank space at the bottom of the escalator, and one extra row + * above the header. + * + * Experimentation idea #1: calculate "scrollbottom" vs content + * bottom and remove one row from top, rest from bottom. This + * FAILED, since setHeight has already happened, thus we never + * will detect ourselves having been scrolled all the way to the + * bottom. + */ + + if (!visualRowOrder.isEmpty()) { + final double firstRowTop = getRowTop( + visualRowOrder.getFirst()); + final double firstRowMinTop = tBodyScrollTop + - getDefaultRowHeight(); + if (firstRowTop < firstRowMinTop) { + final int newLogicalIndex = getLogicalRowIndex( + visualRowOrder.getLast()) + 1; + moveAndUpdateEscalatorRows(Range.withOnly(0), + visualRowOrder.size(), newLogicalIndex); + } + } + } + + if (neededEscalatorRowsDiff != 0) { + fireRowVisibilityChangeEvent(); + } + + Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount"); + } + + @Override + protected void reapplyDefaultRowHeights() { + if (visualRowOrder.isEmpty()) { + return; + } + + Profiler.enter( + "Escalator.BodyRowContainer.reapplyDefaultRowHeights"); + + /* step 1: resize and reposition rows */ + for (int i = 0; i < visualRowOrder.size(); i++) { + TableRowElement tr = visualRowOrder.get(i); + reapplyRowHeight(tr, getDefaultRowHeight()); + + final int logicalIndex = getTopRowLogicalIndex() + i; + setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight()); + } + + /* + * step 2: move scrollbar so that it corresponds to its previous + * place + */ + + /* + * This ratio needs to be calculated with the scrollsize (not max + * scroll position) in order to align the top row with the new + * scroll position. + */ + double scrollRatio = verticalScrollbar.getScrollPos() + / verticalScrollbar.getScrollSize(); + scroller.recalculateScrollbarsForVirtualViewport(); + verticalScrollbar.setScrollPos((int) (getDefaultRowHeight() + * getRowCount() * scrollRatio)); + setBodyScrollPosition(horizontalScrollbar.getScrollPos(), + verticalScrollbar.getScrollPos()); + scroller.onScroll(); + + /* + * step 3: make sure we have the correct amount of escalator rows. + */ + verifyEscalatorCount(); + + int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) + / getDefaultRowHeight()); + setTopRowLogicalIndex(logicalLogical); + + Profiler.leave( + "Escalator.BodyRowContainer.reapplyDefaultRowHeights"); + } + + /** + * Sorts the rows in the DOM to correspond to the visual order. + * + * @see #visualRowOrder + */ + private void sortDomElements() { + final String profilingName = "Escalator.BodyRowContainer.sortDomElements"; + Profiler.enter(profilingName); + + /* + * Focus is lost from an element if that DOM element is (or any of + * its parents are) removed from the document. Therefore, we sort + * everything around that row instead. + */ + final TableRowElement focusedRow = getRowWithFocus(); + + if (focusedRow != null) { + assert focusedRow + .getParentElement() == root : "Trying to sort around a row that doesn't exist in body"; + assert visualRowOrder.contains(focusedRow) + || body.spacerContainer.isSpacer( + focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder or is not a spacer."; + } + + /* + * Two cases handled simultaneously: + * + * 1) No focus on rows. We iterate visualRowOrder backwards, and + * take the respective element in the DOM, and place it as the first + * child in the body element. Then we take the next-to-last from + * visualRowOrder, and put that first, pushing the previous row as + * the second child. And so on... + * + * 2) Focus on some row within Escalator body. Again, we iterate + * visualRowOrder backwards. This time, we use the focused row as a + * pivot: Instead of placing rows from the bottom of visualRowOrder + * and placing it first, we place it underneath the focused row. + * Once we hit the focused row, we don't move it (to not reset + * focus) but change sorting mode. After that, we place all rows as + * the first child. + */ + + List<TableRowElement> orderedBodyRows = new ArrayList<TableRowElement>( + visualRowOrder); + Map<Integer, SpacerContainer.SpacerImpl> spacers = body.spacerContainer + .getSpacers(); + + /* + * Start at -1 to include a spacer that is rendered above the + * viewport, but its parent row is still not shown + */ + for (int i = -1; i < visualRowOrder.size(); i++) { + SpacerContainer.SpacerImpl spacer = spacers + .remove(Integer.valueOf(getTopRowLogicalIndex() + i)); + + if (spacer != null) { + orderedBodyRows.add(i + 1, spacer.getRootElement()); + spacer.show(); + } + } + /* + * At this point, invisible spacers aren't reordered, so their + * position in the DOM will remain undefined. + */ + + // If a spacer was not reordered, it means that it's out of view. + for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) { + unmovedSpacer.hide(); + } + + /* + * If we have a focused row, start in the mode where we put + * everything underneath that row. Otherwise, all rows are placed as + * first child. + */ + boolean insertFirst = (focusedRow == null); + + final ListIterator<TableRowElement> i = orderedBodyRows + .listIterator(orderedBodyRows.size()); + while (i.hasPrevious()) { + TableRowElement tr = i.previous(); + + if (tr == focusedRow) { + insertFirst = true; + } else if (insertFirst) { + root.insertFirst(tr); + } else { + root.insertAfter(tr, focusedRow); + } + } + + Profiler.leave(profilingName); + } + + /** + * Get the {@literal <tbody>} row that contains (or has) focus. + * + * @return The {@literal <tbody>} row that contains a focused DOM + * element, or <code>null</code> if focus is outside of a body + * row. + */ + private TableRowElement getRowWithFocus() { + TableRowElement rowContainingFocus = null; + + final Element focusedElement = WidgetUtil.getFocusedElement(); + + if (focusedElement != null && root.isOrHasChild(focusedElement)) { + Element e = focusedElement; + + while (e != null && e != root) { + /* + * You never know if there's several tables embedded in a + * cell... We'll take the deepest one. + */ + if (TableRowElement.is(e)) { + rowContainingFocus = TableRowElement.as(e); + } + e = e.getParentElement(); + } + } + + return rowContainingFocus; + } + + @Override + public Cell getCell(Element element) { + Cell cell = super.getCell(element); + if (cell == null) { + return null; + } + + // Convert DOM coordinates to logical coordinates for rows + TableRowElement rowElement = (TableRowElement) cell.getElement() + .getParentElement(); + return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(), + cell.getElement()); + } + + @Override + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + spacerContainer.setSpacer(rowIndex, height); + } + + @Override + public void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException { + spacerContainer.setSpacerUpdater(spacerUpdater); + } + + @Override + public SpacerUpdater getSpacerUpdater() { + return spacerContainer.getSpacerUpdater(); + } + + /** + * <em>Calculates</em> the correct top position of a row at a logical + * index, regardless if there is one there or not. + * <p> + * A correct result requires that both {@link #getDefaultRowHeight()} is + * consistent, and the placement and height of all spacers above the + * given logical index are consistent. + * + * @param logicalIndex + * the logical index of the row for which to calculate the + * top position + * @return the position at which to place a row in {@code logicalIndex} + * @see #getRowTop(TableRowElement) + */ + private double getRowTop(int logicalIndex) { + double top = spacerContainer + .getSpacerHeightsSumUntilIndex(logicalIndex); + return top + (logicalIndex * getDefaultRowHeight()); + } + + public void shiftRowPositions(int row, double diff) { + for (TableRowElement tr : getVisibleRowsAfter(row)) { + setRowPosition(tr, 0, getRowTop(tr) + diff); + } + } + + private List<TableRowElement> getVisibleRowsAfter(int logicalRow) { + Range visibleRowLogicalRange = getVisibleRowRange(); + + boolean allRowsAreInView = logicalRow < visibleRowLogicalRange + .getStart(); + boolean noRowsAreInView = logicalRow >= visibleRowLogicalRange + .getEnd() - 1; + + if (allRowsAreInView) { + return Collections.unmodifiableList(visualRowOrder); + } else if (noRowsAreInView) { + return Collections.emptyList(); + } else { + int fromIndex = (logicalRow - visibleRowLogicalRange.getStart()) + + 1; + int toIndex = visibleRowLogicalRange.length(); + List<TableRowElement> sublist = visualRowOrder + .subList(fromIndex, toIndex); + return Collections.unmodifiableList(sublist); + } + } + + @Override + public int getDomRowCount() { + return root.getChildCount() + - spacerContainer.getSpacersInDom().size(); + } + + @Override + protected boolean rowCanBeFrozen(TableRowElement tr) { + return visualRowOrder.contains(tr); + } + + void reapplySpacerWidths() { + spacerContainer.reapplySpacerWidths(); + } + + void scrollToSpacer(int spacerIndex, ScrollDestination destination, + int padding) { + spacerContainer.scrollToSpacer(spacerIndex, destination, padding); + } + } + + private class ColumnConfigurationImpl implements ColumnConfiguration { + public class Column { + public static final double DEFAULT_COLUMN_WIDTH_PX = 100; + + private double definedWidth = -1; + private double calculatedWidth = DEFAULT_COLUMN_WIDTH_PX; + private boolean measuringRequested = false; + + public void setWidth(double px) { + definedWidth = px; + + if (px < 0) { + if (isAttached()) { + calculateWidth(); + } else { + /* + * the column's width is calculated at Escalator.onLoad + * via measureAndSetWidthIfNeeded! + */ + measuringRequested = true; + } + } else { + calculatedWidth = px; + } + } + + public double getDefinedWidth() { + return definedWidth; + } + + /** + * Returns the actual width in the DOM. + * + * @return the width in pixels in the DOM. Returns -1 if the column + * needs measuring, but has not been yet measured + */ + public double getCalculatedWidth() { + /* + * This might return an untrue value (e.g. during init/onload), + * since we haven't had a proper chance to actually calculate + * widths yet. + * + * This is fixed during Escalator.onLoad, by the call to + * "measureAndSetWidthIfNeeded", which fixes "everything". + */ + if (!measuringRequested) { + return calculatedWidth; + } else { + return -1; + } + } + + /** + * Checks if the column needs measuring, and then measures it. + * <p> + * Called by {@link Escalator#onLoad()}. + */ + public boolean measureAndSetWidthIfNeeded() { + assert isAttached() : "Column.measureAndSetWidthIfNeeded() was called even though Escalator was not attached!"; + + if (measuringRequested) { + measuringRequested = false; + setWidth(definedWidth); + return true; + } + return false; + } + + private void calculateWidth() { + calculatedWidth = getMaxCellWidth(columns.indexOf(this)); + } + } + + private final List<Column> columns = new ArrayList<Column>(); + private int frozenColumns = 0; + + /* + * TODO: this is a bit of a duplicate functionality with the + * Column.calculatedWidth caching. Probably should use one or the other, + * not both + */ + /** + * A cached array of all the calculated column widths. + * + * @see #getCalculatedColumnWidths() + */ + private double[] widthsArray = null; + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there are no rows in the DOM when + * this method is called. + * + * @see #hasSomethingInDom() + */ + @Override + public void removeColumns(final int index, final int numberOfColumns) { + // Validate + assertArgumentsAreValidAndWithinRange(index, numberOfColumns); + + // Move the horizontal scrollbar to the left, if removed columns are + // to the left of the viewport + removeColumnsAdjustScrollbar(index, numberOfColumns); + + // Remove from DOM + header.paintRemoveColumns(index, numberOfColumns); + body.paintRemoveColumns(index, numberOfColumns); + footer.paintRemoveColumns(index, numberOfColumns); + + // Remove from bookkeeping + flyweightRow.removeCells(index, numberOfColumns); + columns.subList(index, index + numberOfColumns).clear(); + + // Adjust frozen columns + if (index < getFrozenColumnCount()) { + if (index + numberOfColumns < frozenColumns) { + /* + * Last removed column was frozen, meaning that all removed + * columns were frozen. Just decrement the number of frozen + * columns accordingly. + */ + frozenColumns -= numberOfColumns; + } else { + /* + * If last removed column was not frozen, we have removed + * columns beyond the frozen range, so all remaining frozen + * columns are to the left of the removed columns. + */ + frozenColumns = index; + } + } + + scroller.recalculateScrollbarsForVirtualViewport(); + body.verifyEscalatorCount(); + + if (getColumnConfiguration().getColumnCount() > 0) { + reapplyRowWidths(header); + reapplyRowWidths(body); + reapplyRowWidths(footer); + } + + /* + * Colspans make any kind of automatic clever content re-rendering + * impossible: As soon as anything has colspans, removing one might + * reveal further colspans, modifying the DOM structure once again, + * ending in a cascade of updates. Because we don't know how the + * data is updated. + * + * So, instead, we don't do anything. The client code is responsible + * for re-rendering the content (if so desired). Everything Just + * Works (TM) if colspans aren't used. + */ + } + + private void reapplyRowWidths(AbstractRowContainer container) { + if (container.getRowCount() > 0) { + container.reapplyRowWidths(); + } + } + + private void removeColumnsAdjustScrollbar(int index, + int numberOfColumns) { + if (horizontalScrollbar.getOffsetSize() >= horizontalScrollbar + .getScrollSize()) { + return; + } + + double leftPosOfFirstColumnToRemove = getCalculatedColumnsWidth( + Range.between(0, index)); + double widthOfColumnsToRemove = getCalculatedColumnsWidth( + Range.withLength(index, numberOfColumns)); + + double scrollLeft = horizontalScrollbar.getScrollPos(); + + if (scrollLeft <= leftPosOfFirstColumnToRemove) { + /* + * viewport is scrolled to the left of the first removed column, + * so there's no need to adjust anything + */ + return; + } + + double adjustedScrollLeft = Math.max(leftPosOfFirstColumnToRemove, + scrollLeft - widthOfColumnsToRemove); + horizontalScrollbar.setScrollPos(adjustedScrollLeft); + } + + /** + * Calculate the width of a row, as the sum of columns' widths. + * + * @return the width of a row, in pixels + */ + public double calculateRowWidth() { + return getCalculatedColumnsWidth( + Range.between(0, getColumnCount())); + } + + private void assertArgumentsAreValidAndWithinRange(final int index, + final int numberOfColumns) { + if (numberOfColumns < 1) { + throw new IllegalArgumentException( + "Number of columns can't be less than 1 (was " + + numberOfColumns + ")"); + } + + if (index < 0 || index + numberOfColumns > getColumnCount()) { + throw new IndexOutOfBoundsException("The given " + + "column range (" + index + ".." + + (index + numberOfColumns) + + ") was outside of the current " + + "number of columns (" + getColumnCount() + ")"); + } + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for rows when this + * method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void insertColumns(final int index, final int numberOfColumns) { + // Validate + if (index < 0 || index > getColumnCount()) { + throw new IndexOutOfBoundsException("The given index(" + index + + ") was outside of the current number of columns (0.." + + getColumnCount() + ")"); + } + + if (numberOfColumns < 1) { + throw new IllegalArgumentException( + "Number of columns must be 1 or greater (was " + + numberOfColumns); + } + + // Add to bookkeeping + flyweightRow.addCells(index, numberOfColumns); + for (int i = 0; i < numberOfColumns; i++) { + columns.add(index, new Column()); + } + + // Adjust frozen columns + boolean frozen = index < frozenColumns; + if (frozen) { + frozenColumns += numberOfColumns; + } + + // this needs to be before the scrollbar adjustment. + boolean scrollbarWasNeeded = horizontalScrollbar + .getOffsetSize() < horizontalScrollbar.getScrollSize(); + scroller.recalculateScrollbarsForVirtualViewport(); + boolean scrollbarIsNowNeeded = horizontalScrollbar + .getOffsetSize() < horizontalScrollbar.getScrollSize(); + if (!scrollbarWasNeeded && scrollbarIsNowNeeded) { + body.verifyEscalatorCount(); + } + + // Add to DOM + header.paintInsertColumns(index, numberOfColumns, frozen); + body.paintInsertColumns(index, numberOfColumns, frozen); + footer.paintInsertColumns(index, numberOfColumns, frozen); + + // fix initial width + if (header.getRowCount() > 0 || body.getRowCount() > 0 + || footer.getRowCount() > 0) { + + Map<Integer, Double> colWidths = new HashMap<Integer, Double>(); + Double width = Double.valueOf(Column.DEFAULT_COLUMN_WIDTH_PX); + for (int i = index; i < index + numberOfColumns; i++) { + Integer col = Integer.valueOf(i); + colWidths.put(col, width); + } + getColumnConfiguration().setColumnWidths(colWidths); + } + + // Adjust scrollbar + double pixelsToInsertedColumn = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, index)); + final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn; + + if (columnsWereAddedToTheLeftOfViewport) { + double insertedColumnsWidth = columnConfiguration + .getCalculatedColumnsWidth( + Range.withLength(index, numberOfColumns)); + horizontalScrollbar.setScrollPos( + scroller.lastScrollLeft + insertedColumnsWidth); + } + + /* + * Colspans make any kind of automatic clever content re-rendering + * impossible: As soon as anything has colspans, adding one might + * affect surrounding colspans, modifying the DOM structure once + * again, ending in a cascade of updates. Because we don't know how + * the data is updated. + * + * So, instead, we don't do anything. The client code is responsible + * for re-rendering the content (if so desired). Everything Just + * Works (TM) if colspans aren't used. + */ + } + + @Override + public int getColumnCount() { + return columns.size(); + } + + @Override + public void setFrozenColumnCount(int count) + throws IllegalArgumentException { + if (count < 0 || count > getColumnCount()) { + throw new IllegalArgumentException( + "count must be between 0 and the current number of columns (" + + getColumnCount() + ")"); + } + int oldCount = frozenColumns; + if (count == oldCount) { + return; + } + + frozenColumns = count; + + if (hasSomethingInDom()) { + // Are we freezing or unfreezing? + boolean frozen = count > oldCount; + + int firstAffectedCol; + int firstUnaffectedCol; + + if (frozen) { + firstAffectedCol = oldCount; + firstUnaffectedCol = count; + } else { + firstAffectedCol = count; + firstUnaffectedCol = oldCount; + } + + if (oldCount > 0) { + header.setColumnLastFrozen(oldCount - 1, false); + body.setColumnLastFrozen(oldCount - 1, false); + footer.setColumnLastFrozen(oldCount - 1, false); + } + if (count > 0) { + header.setColumnLastFrozen(count - 1, true); + body.setColumnLastFrozen(count - 1, true); + footer.setColumnLastFrozen(count - 1, true); + } + + for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) { + header.setColumnFrozen(col, frozen); + body.setColumnFrozen(col, frozen); + footer.setColumnFrozen(col, frozen); + } + } + + scroller.recalculateScrollbarsForVirtualViewport(); + } + + @Override + public int getFrozenColumnCount() { + return frozenColumns; + } + + @Override + public void setColumnWidth(int index, double px) + throws IllegalArgumentException { + setColumnWidths(Collections.singletonMap(Integer.valueOf(index), + Double.valueOf(px))); + } + + @Override + public void setColumnWidths(Map<Integer, Double> indexWidthMap) + throws IllegalArgumentException { + + if (indexWidthMap == null) { + throw new IllegalArgumentException("indexWidthMap was null"); + } + + if (indexWidthMap.isEmpty()) { + return; + } + + for (Entry<Integer, Double> entry : indexWidthMap.entrySet()) { + int index = entry.getKey().intValue(); + double width = entry.getValue().doubleValue(); + + checkValidColumnIndex(index); + + // Not all browsers will accept any fractional size.. + width = WidgetUtil.roundSizeDown(width); + columns.get(index).setWidth(width); + + } + + widthsArray = null; + header.reapplyColumnWidths(); + body.reapplyColumnWidths(); + footer.reapplyColumnWidths(); + + recalculateElementSizes(); + } + + private void checkValidColumnIndex(int index) + throws IllegalArgumentException { + if (!Range.withLength(0, getColumnCount()).contains(index)) { + throw new IllegalArgumentException("The given column index (" + + index + ") does not exist"); + } + } + + @Override + public double getColumnWidth(int index) + throws IllegalArgumentException { + checkValidColumnIndex(index); + return columns.get(index).getDefinedWidth(); + } + + @Override + public double getColumnWidthActual(int index) { + return columns.get(index).getCalculatedWidth(); + } + + private double getMaxCellWidth(int colIndex) + throws IllegalArgumentException { + double headerWidth = header.measureMinCellWidth(colIndex, true); + double bodyWidth = body.measureMinCellWidth(colIndex, true); + double footerWidth = footer.measureMinCellWidth(colIndex, true); + + double maxWidth = Math.max(headerWidth, + Math.max(bodyWidth, footerWidth)); + assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible."; + return maxWidth; + } + + private double getMinCellWidth(int colIndex) + throws IllegalArgumentException { + double headerWidth = header.measureMinCellWidth(colIndex, false); + double bodyWidth = body.measureMinCellWidth(colIndex, false); + double footerWidth = footer.measureMinCellWidth(colIndex, false); + + double minWidth = Math.max(headerWidth, + Math.max(bodyWidth, footerWidth)); + assert minWidth >= 0 : "Got a negative max width for a column, which should be impossible."; + return minWidth; + } + + /** + * Calculates the width of the columns in a given range. + * + * @param columns + * the columns to calculate + * @return the total width of the columns in the given + * <code>columns</code> + */ + double getCalculatedColumnsWidth(final Range columns) { + /* + * This is an assert instead of an exception, since this is an + * internal method. + */ + assert columns + .isSubsetOf(Range.between(0, getColumnCount())) : "Range " + + "was outside of current column range (i.e.: " + + Range.between(0, getColumnCount()) + + ", but was given :" + columns; + + double sum = 0; + for (int i = columns.getStart(); i < columns.getEnd(); i++) { + double columnWidthActual = getColumnWidthActual(i); + sum += columnWidthActual; + } + return sum; + } + + double[] getCalculatedColumnWidths() { + if (widthsArray == null || widthsArray.length != getColumnCount()) { + widthsArray = new double[getColumnCount()]; + for (int i = 0; i < columns.size(); i++) { + widthsArray[i] = columns.get(i).getCalculatedWidth(); + } + } + return widthsArray; + } + + @Override + public void refreshColumns(int index, int numberOfColumns) + throws IndexOutOfBoundsException, IllegalArgumentException { + if (numberOfColumns < 1) { + throw new IllegalArgumentException( + "Number of columns must be 1 or greater (was " + + numberOfColumns + ")"); + } + + if (index < 0 || index + numberOfColumns > getColumnCount()) { + throw new IndexOutOfBoundsException( + "The given " + "column range (" + index + ".." + + (index + numberOfColumns) + + ") was outside of the current number of columns (" + + getColumnCount() + ")"); + } + + header.refreshColumns(index, numberOfColumns); + body.refreshColumns(index, numberOfColumns); + footer.refreshColumns(index, numberOfColumns); + } + } + + /** + * A decision on how to measure a spacer when it is partially within a + * designated range. + * <p> + * The meaning of each value may differ depending on the context it is being + * used in. Check that particular method's JavaDoc. + */ + private enum SpacerInclusionStrategy { + /** A representation of "the entire spacer". */ + COMPLETE, + + /** A representation of "a partial spacer". */ + PARTIAL, + + /** A representation of "no spacer at all". */ + NONE + } + + private class SpacerContainer { + + /** This is used mainly for testing purposes */ + private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow"; + + private final class SpacerImpl implements Spacer { + private TableCellElement spacerElement; + private TableRowElement root; + private DivElement deco; + private int rowIndex; + private double height = -1; + private boolean domHasBeenSetup = false; + private double decoHeight; + private double defaultCellBorderBottomSize = -1; + + public SpacerImpl(int rowIndex) { + this.rowIndex = rowIndex; + + root = TableRowElement.as(DOM.createTR()); + spacerElement = TableCellElement.as(DOM.createTD()); + root.appendChild(spacerElement); + root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); + deco = DivElement.as(DOM.createDiv()); + } + + public void setPositionDiff(double x, double y) { + setPosition(getLeft() + x, getTop() + y); + } + + public void setupDom(double height) { + assert !domHasBeenSetup : "DOM can't be set up twice."; + assert RootPanel.get().getElement().isOrHasChild( + root) : "Root element should've been attached to the DOM by now."; + domHasBeenSetup = true; + + getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX); + setHeight(height); + + spacerElement + .setColSpan(getColumnConfiguration().getColumnCount()); + + setStylePrimaryName(getStylePrimaryName()); + } + + public TableRowElement getRootElement() { + return root; + } + + @Override + public Element getDecoElement() { + return deco; + } + + public void setPosition(double x, double y) { + positions.set(getRootElement(), x, y); + positions.set(getDecoElement(), 0, + y - getSpacerDecoTopOffset()); + } + + private double getSpacerDecoTopOffset() { + return getBody().getDefaultRowHeight(); + } + + public void setStylePrimaryName(String style) { + UIObject.setStylePrimaryName(root, style + "-spacer"); + UIObject.setStylePrimaryName(deco, style + "-spacer-deco"); + } + + public void setHeight(double height) { + + assert height >= 0 : "Height must be more >= 0 (was " + height + + ")"; + + final double heightDiff = height - Math.max(0, this.height); + final double oldHeight = this.height; + + this.height = height; + + // since the spacer might be rendered on top of the previous + // rows border (done with css), need to increase height the + // amount of the border thickness + if (defaultCellBorderBottomSize < 0) { + defaultCellBorderBottomSize = WidgetUtil + .getBorderBottomThickness(body + .getRowElement( + getVisibleRowRange().getStart()) + .getFirstChildElement()); + } + root.getStyle().setHeight(height + defaultCellBorderBottomSize, + Unit.PX); + + // move the visible spacers getRow row onwards. + shiftSpacerPositionsAfterRow(getRow(), heightDiff); + + /* + * If we're growing, we'll adjust the scroll size first, then + * adjust scrolling. If we're shrinking, we do it after the + * second if-clause. + */ + boolean spacerIsGrowing = heightDiff > 0; + if (spacerIsGrowing) { + verticalScrollbar.setScrollSize( + verticalScrollbar.getScrollSize() + heightDiff); + } + + /* + * Don't modify the scrollbars if we're expanding the -1 spacer + * while we're scrolled to the top. + */ + boolean minusOneSpacerException = spacerIsGrowing + && getRow() == -1 && body.getTopRowLogicalIndex() == 0; + + boolean viewportNeedsScrolling = getRow() < body + .getTopRowLogicalIndex() && !minusOneSpacerException; + if (viewportNeedsScrolling) { + + /* + * We can't use adjustScrollPos here, probably because of a + * bookkeeping-related race condition. + * + * This particular situation is easier, however, since we + * know exactly how many pixels we need to move (heightDiff) + * and all elements below the spacer always need to move + * that pixel amount. + */ + + for (TableRowElement row : body.visualRowOrder) { + body.setRowPosition(row, 0, + body.getRowTop(row) + heightDiff); + } + + double top = getTop(); + double bottom = top + oldHeight; + double scrollTop = verticalScrollbar.getScrollPos(); + + boolean viewportTopIsAtMidSpacer = top < scrollTop + && scrollTop < bottom; + + final double moveDiff; + if (viewportTopIsAtMidSpacer && !spacerIsGrowing) { + + /* + * If the scroll top is in the middle of the modified + * spacer, we want to scroll the viewport up as usual, + * but we don't want to scroll past the top of it. + * + * Math.max ensures this (remember: the result is going + * to be negative). + */ + + moveDiff = Math.max(heightDiff, top - scrollTop); + } else { + moveDiff = heightDiff; + } + body.setBodyScrollPosition(tBodyScrollLeft, + tBodyScrollTop + moveDiff); + verticalScrollbar.setScrollPosByDelta(moveDiff); + + } else { + body.shiftRowPositions(getRow(), heightDiff); + } + + if (!spacerIsGrowing) { + verticalScrollbar.setScrollSize( + verticalScrollbar.getScrollSize() + heightDiff); + } + + updateDecoratorGeometry(height); + } + + /** Resizes and places the decorator. */ + private void updateDecoratorGeometry(double detailsHeight) { + Style style = deco.getStyle(); + decoHeight = detailsHeight + getBody().getDefaultRowHeight(); + style.setHeight(decoHeight, Unit.PX); + } + + @Override + public Element getElement() { + return spacerElement; + } + + @Override + public int getRow() { + return rowIndex; + } + + public double getHeight() { + assert height >= 0 : "Height was not previously set by setHeight."; + return height; + } + + public double getTop() { + return positions.getTop(getRootElement()); + } + + public double getLeft() { + return positions.getLeft(getRootElement()); + } + + /** + * Sets a new row index for this spacer. Also updates the bookeeping + * at {@link SpacerContainer#rowIndexToSpacer}. + */ + @SuppressWarnings("boxing") + public void setRowIndex(int rowIndex) { + SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex); + assert this == spacer : "trying to move an unexpected spacer."; + this.rowIndex = rowIndex; + root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); + rowIndexToSpacer.put(this.rowIndex, this); + } + + /** + * Updates the spacer's visibility parameters, based on whether it + * is being currently visible or not. + */ + public void updateVisibility() { + if (isInViewport()) { + show(); + } else { + hide(); + } + } + + private boolean isInViewport() { + int top = (int) Math.ceil(getTop()); + int height = (int) Math.floor(getHeight()); + Range location = Range.withLength(top, height); + return getViewportPixels().intersects(location); + } + + public void show() { + getRootElement().getStyle().clearDisplay(); + getDecoElement().getStyle().clearDisplay(); + } + + public void hide() { + getRootElement().getStyle().setDisplay(Display.NONE); + getDecoElement().getStyle().setDisplay(Display.NONE); + } + + /** + * Crop the decorator element so that it doesn't overlap the header + * and footer sections. + * + * @param bodyTop + * the top cordinate of the escalator body + * @param bodyBottom + * the bottom cordinate of the escalator body + * @param decoWidth + * width of the deco + */ + private void updateDecoClip(final double bodyTop, + final double bodyBottom, final double decoWidth) { + final int top = deco.getAbsoluteTop(); + final int bottom = deco.getAbsoluteBottom(); + /* + * FIXME + * + * Height and its use is a workaround for the issue where + * coordinates of the deco are not calculated yet. This will + * prevent a deco from being displayed when it's added to DOM + */ + final int height = bottom - top; + if (top < bodyTop || bottom > bodyBottom) { + final double topClip = Math.max(0.0D, bodyTop - top); + final double bottomClip = height + - Math.max(0.0D, bottom - bodyBottom); + // TODO [optimize] not sure how GWT compiles this + final String clip = new StringBuilder("rect(") + .append(topClip).append("px,").append(decoWidth) + .append("px,").append(bottomClip).append("px,0)") + .toString(); + deco.getStyle().setProperty("clip", clip); + } else { + deco.getStyle().setProperty("clip", "auto"); + } + } + } + + private final TreeMap<Integer, SpacerImpl> rowIndexToSpacer = new TreeMap<Integer, SpacerImpl>(); + + private SpacerUpdater spacerUpdater = SpacerUpdater.NULL; + + private final ScrollHandler spacerScroller = new ScrollHandler() { + private double prevScrollX = 0; + + @Override + public void onScroll(ScrollEvent event) { + if (WidgetUtil.pixelValuesEqual(getScrollLeft(), prevScrollX)) { + return; + } + + prevScrollX = getScrollLeft(); + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.setPosition(prevScrollX, spacer.getTop()); + } + } + }; + private HandlerRegistration spacerScrollerRegistration; + + /** Width of the spacers' decos. Calculated once then cached. */ + private double spacerDecoWidth = 0.0D; + + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + + if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) { + throw new IllegalArgumentException("invalid row index: " + + rowIndex + ", while the body only has " + + getBody().getRowCount() + " rows."); + } + + if (height >= 0) { + if (!spacerExists(rowIndex)) { + insertNewSpacer(rowIndex, height); + } else { + updateExistingSpacer(rowIndex, height); + } + } else if (spacerExists(rowIndex)) { + removeSpacer(rowIndex); + } + + updateSpacerDecosVisibility(); + } + + /** Checks if a given element is a spacer element */ + public boolean isSpacer(Element row) { + + /* + * If this needs optimization, we could do a more heuristic check + * based on stylenames and stuff, instead of iterating through the + * map. + */ + + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + if (spacer.getRootElement().equals(row)) { + return true; + } + } + + return false; + } + + @SuppressWarnings("boxing") + void scrollToSpacer(int spacerIndex, ScrollDestination destination, + int padding) { + + assert !destination.equals(ScrollDestination.MIDDLE) + || padding != 0 : "destination/padding check should be done before this method"; + + if (!rowIndexToSpacer.containsKey(spacerIndex)) { + throw new IllegalArgumentException( + "No spacer open at index " + spacerIndex); + } + + SpacerImpl spacer = rowIndexToSpacer.get(spacerIndex); + double targetStartPx = spacer.getTop(); + double targetEndPx = targetStartPx + spacer.getHeight(); + + Range viewportPixels = getViewportPixels(); + double viewportStartPx = viewportPixels.getStart(); + double viewportEndPx = viewportPixels.getEnd(); + + double scrollTop = getScrollPos(destination, targetStartPx, + targetEndPx, viewportStartPx, viewportEndPx, padding); + + setScrollTop(scrollTop); + } + + public void reapplySpacerWidths() { + // FIXME #16266 , spacers get couple pixels too much because borders + final double width = getInnerWidth() - spacerDecoWidth; + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.getRootElement().getStyle().setWidth(width, Unit.PX); + } + } + + public void paintRemoveSpacers(Range removedRowsRange) { + removeSpacers(removedRowsRange); + shiftSpacersByRows(removedRowsRange.getStart(), + -removedRowsRange.length()); + } + + @SuppressWarnings("boxing") + public void removeSpacers(Range removedRange) { + + Map<Integer, SpacerImpl> removedSpacers = rowIndexToSpacer.subMap( + removedRange.getStart(), true, removedRange.getEnd(), + false); + + if (removedSpacers.isEmpty()) { + return; + } + + for (SpacerImpl spacer : removedSpacers.values()) { + /* + * [[optimization]] TODO: Each invocation of the setHeight + * method has a cascading effect in the DOM. if this proves to + * be slow, the DOM offset could be updated as a batch. + */ + + destroySpacerContent(spacer); + spacer.setHeight(0); // resets row offsets + spacer.getRootElement().removeFromParent(); + spacer.getDecoElement().removeFromParent(); + } + + removedSpacers.clear(); + + if (rowIndexToSpacer.isEmpty()) { + assert spacerScrollerRegistration != null : "Spacer scroller registration was null"; + spacerScrollerRegistration.removeHandler(); + spacerScrollerRegistration = null; + } + } + + public Map<Integer, SpacerImpl> getSpacers() { + return new HashMap<Integer, SpacerImpl>(rowIndexToSpacer); + } + + /** + * Calculates the sum of all spacers. + * + * @return sum of all spacers, or 0 if no spacers present + */ + public double getSpacerHeightsSum() { + return getHeights(rowIndexToSpacer.values()); + } + + /** + * Calculates the sum of all spacers from one row index onwards. + * + * @param logicalRowIndex + * the spacer to include as the first calculated spacer + * @return the sum of all spacers from {@code logicalRowIndex} and + * onwards, or 0 if no suitable spacers were found + */ + @SuppressWarnings("boxing") + public Collection<SpacerImpl> getSpacersForRowAndAfter( + int logicalRowIndex) { + return new ArrayList<SpacerImpl>( + rowIndexToSpacer.tailMap(logicalRowIndex, true).values()); + } + + /** + * Get all spacers from one pixel point onwards. + * <p> + * + * In this method, the {@link SpacerInclusionStrategy} has the following + * meaning when a spacer lies in the middle of either pixel argument: + * <dl> + * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE} + * <dd>include the spacer + * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL} + * <dd>include the spacer + * <dt>{@link SpacerInclusionStrategy#NONE NONE} + * <dd>ignore the spacer + * </dl> + * + * @param px + * the pixel point after which to return all spacers + * @param strategy + * the inclusion strategy regarding the {@code px} + * @return a collection of the spacers that exist after {@code px} + */ + public Collection<SpacerImpl> getSpacersAfterPx(final double px, + final SpacerInclusionStrategy strategy) { + + ArrayList<SpacerImpl> spacers = new ArrayList<SpacerImpl>( + rowIndexToSpacer.values()); + + for (int i = 0; i < spacers.size(); i++) { + SpacerImpl spacer = spacers.get(i); + + double top = spacer.getTop(); + double bottom = top + spacer.getHeight(); + + if (top > px) { + return spacers.subList(i, spacers.size()); + } else if (bottom > px) { + if (strategy == SpacerInclusionStrategy.NONE) { + return spacers.subList(i + 1, spacers.size()); + } else { + return spacers.subList(i, spacers.size()); + } + } + } + + return Collections.emptySet(); + } + + /** + * Gets the spacers currently rendered in the DOM. + * + * @return an unmodifiable (but live) collection of the spacers + * currently in the DOM + */ + public Collection<SpacerImpl> getSpacersInDom() { + return Collections + .unmodifiableCollection(rowIndexToSpacer.values()); + } + + /** + * Gets the amount of pixels occupied by spacers between two pixel + * points. + * <p> + * In this method, the {@link SpacerInclusionStrategy} has the following + * meaning when a spacer lies in the middle of either pixel argument: + * <dl> + * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE} + * <dd>take the entire spacer into account + * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL} + * <dd>take only the visible area into account + * <dt>{@link SpacerInclusionStrategy#NONE NONE} + * <dd>ignore that spacer + * </dl> + * + * @param rangeTop + * the top pixel point + * @param topInclusion + * the inclusion strategy regarding {@code rangeTop}. + * @param rangeBottom + * the bottom pixel point + * @param bottomInclusion + * the inclusion strategy regarding {@code rangeBottom}. + * @return the pixels occupied by spacers between {@code rangeTop} and + * {@code rangeBottom} + */ + public double getSpacerHeightsSumBetweenPx(double rangeTop, + SpacerInclusionStrategy topInclusion, double rangeBottom, + SpacerInclusionStrategy bottomInclusion) { + + assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom"; + + double heights = 0; + + /* + * TODO [[optimize]]: this might be somewhat inefficient (due to + * iterator-based scanning, instead of using the treemap's search + * functionalities). But it should be easy to write, read, verify + * and maintain. + */ + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + double top = spacer.getTop(); + double height = spacer.getHeight(); + double bottom = top + height; + + /* + * If we happen to implement a DoubleRange (in addition to the + * int-based Range) at some point, the following logic should + * probably be converted into using the + * Range.partitionWith-equivalent. + */ + + boolean topIsAboveRange = top < rangeTop; + boolean topIsInRange = rangeTop <= top && top <= rangeBottom; + boolean topIsBelowRange = rangeBottom < top; + + boolean bottomIsAboveRange = bottom < rangeTop; + boolean bottomIsInRange = rangeTop <= bottom + && bottom <= rangeBottom; + boolean bottomIsBelowRange = rangeBottom < bottom; + + assert topIsAboveRange ^ topIsBelowRange + ^ topIsInRange : "Bad top logic"; + assert bottomIsAboveRange ^ bottomIsBelowRange + ^ bottomIsInRange : "Bad bottom logic"; + + if (bottomIsAboveRange) { + continue; + } else if (topIsBelowRange) { + return heights; + } + + else if (topIsAboveRange && bottomIsInRange) { + switch (topInclusion) { + case PARTIAL: + heights += bottom - rangeTop; + break; + case COMPLETE: + heights += height; + break; + default: + break; + } + } + + else if (topIsAboveRange && bottomIsBelowRange) { + + /* + * Here we arbitrarily decide that the top inclusion will + * have the honor of overriding the bottom inclusion if + * happens to be a conflict of interests. + */ + switch (topInclusion) { + case NONE: + return 0; + case COMPLETE: + return height; + case PARTIAL: + return rangeBottom - rangeTop; + default: + throw new IllegalArgumentException( + "Unexpected inclusion state :" + topInclusion); + } + + } else if (topIsInRange && bottomIsInRange) { + heights += height; + } + + else if (topIsInRange && bottomIsBelowRange) { + switch (bottomInclusion) { + case PARTIAL: + heights += rangeBottom - top; + break; + case COMPLETE: + heights += height; + break; + default: + break; + } + + return heights; + } + + else { + assert false : "Unnaccounted-for situation"; + } + } + + return heights; + } + + /** + * Gets the amount of pixels occupied by spacers from the top until a + * certain spot from the top of the body. + * + * @param px + * pixels counted from the top + * @return the pixels occupied by spacers up until {@code px} + */ + public double getSpacerHeightsSumUntilPx(double px) { + return getSpacerHeightsSumBetweenPx(0, + SpacerInclusionStrategy.PARTIAL, px, + SpacerInclusionStrategy.PARTIAL); + } + + /** + * Gets the amount of pixels occupied by spacers until a logical row + * index. + * + * @param logicalIndex + * a logical row index + * @return the pixels occupied by spacers up until {@code logicalIndex} + */ + @SuppressWarnings("boxing") + public double getSpacerHeightsSumUntilIndex(int logicalIndex) { + return getHeights( + rowIndexToSpacer.headMap(logicalIndex, false).values()); + } + + private double getHeights(Collection<SpacerImpl> spacers) { + double heights = 0; + for (SpacerImpl spacer : spacers) { + heights += spacer.getHeight(); + } + return heights; + } + + /** + * Gets the height of the spacer for a row index. + * + * @param rowIndex + * the index of the row where the spacer should be + * @return the height of the spacer at index {@code rowIndex}, or 0 if + * there is no spacer there + */ + public double getSpacerHeight(int rowIndex) { + SpacerImpl spacer = getSpacer(rowIndex); + if (spacer != null) { + return spacer.getHeight(); + } else { + return 0; + } + } + + private boolean spacerExists(int rowIndex) { + return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex)); + } + + @SuppressWarnings("boxing") + private void insertNewSpacer(int rowIndex, double height) { + + if (spacerScrollerRegistration == null) { + spacerScrollerRegistration = addScrollHandler(spacerScroller); + } + + final SpacerImpl spacer = new SpacerImpl(rowIndex); + + rowIndexToSpacer.put(rowIndex, spacer); + // set the position before adding it to DOM + positions.set(spacer.getRootElement(), getScrollLeft(), + calculateSpacerTop(rowIndex)); + + TableRowElement spacerRoot = spacer.getRootElement(); + spacerRoot.getStyle() + .setWidth(columnConfiguration.calculateRowWidth(), Unit.PX); + body.getElement().appendChild(spacerRoot); + spacer.setupDom(height); + // set the deco position, requires that spacer is in the DOM + positions.set(spacer.getDecoElement(), 0, + spacer.getTop() - spacer.getSpacerDecoTopOffset()); + + spacerDecoContainer.appendChild(spacer.getDecoElement()); + if (spacerDecoContainer.getParentElement() == null) { + getElement().appendChild(spacerDecoContainer); + // calculate the spacer deco width, it won't change + spacerDecoWidth = WidgetUtil + .getRequiredWidthBoundingClientRectDouble( + spacer.getDecoElement()); + } + + initSpacerContent(spacer); + + body.sortDomElements(); + } + + private void updateExistingSpacer(int rowIndex, double newHeight) { + getSpacer(rowIndex).setHeight(newHeight); + } + + public SpacerImpl getSpacer(int rowIndex) { + return rowIndexToSpacer.get(Integer.valueOf(rowIndex)); + } + + private void removeSpacer(int rowIndex) { + removeSpacers(Range.withOnly(rowIndex)); + } + + public void setStylePrimaryName(String style) { + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.setStylePrimaryName(style); + } + } + + public void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException { + if (spacerUpdater == null) { + throw new IllegalArgumentException( + "spacer updater cannot be null"); + } + + destroySpacerContent(rowIndexToSpacer.values()); + this.spacerUpdater = spacerUpdater; + initSpacerContent(rowIndexToSpacer.values()); + } + + public SpacerUpdater getSpacerUpdater() { + return spacerUpdater; + } + + private void destroySpacerContent(Iterable<SpacerImpl> spacers) { + for (SpacerImpl spacer : spacers) { + destroySpacerContent(spacer); + } + } + + private void destroySpacerContent(SpacerImpl spacer) { + assert getElement().isOrHasChild(spacer + .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching"; + assert getElement().isOrHasChild(spacer + .getElement()) : "Spacer element somehow got detached from Escalator before detaching"; + spacerUpdater.destroy(spacer); + assert getElement().isOrHasChild(spacer + .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching"; + assert getElement().isOrHasChild(spacer + .getElement()) : "Spacer element somehow got detached from Escalator before detaching"; + } + + private void initSpacerContent(Iterable<SpacerImpl> spacers) { + for (SpacerImpl spacer : spacers) { + initSpacerContent(spacer); + } + } + + private void initSpacerContent(SpacerImpl spacer) { + assert getElement().isOrHasChild(spacer + .getRootElement()) : "Spacer's root element somehow got detached from Escalator before attaching"; + assert getElement().isOrHasChild(spacer + .getElement()) : "Spacer element somehow got detached from Escalator before attaching"; + spacerUpdater.init(spacer); + assert getElement().isOrHasChild(spacer + .getRootElement()) : "Spacer's root element somehow got detached from Escalator during attaching"; + assert getElement().isOrHasChild(spacer + .getElement()) : "Spacer element somehow got detached from Escalator during attaching"; + + spacer.updateVisibility(); + } + + public String getSubPartName(Element subElement) { + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + if (spacer.getRootElement().isOrHasChild(subElement)) { + return "spacer[" + spacer.getRow() + "]"; + } + } + return null; + } + + public Element getSubPartElement(int index) { + SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index)); + if (spacer != null) { + return spacer.getElement(); + } else { + return null; + } + } + + private double calculateSpacerTop(int logicalIndex) { + return body.getRowTop(logicalIndex) + body.getDefaultRowHeight(); + } + + @SuppressWarnings("boxing") + private void shiftSpacerPositionsAfterRow(int changedRowIndex, + double diffPx) { + for (SpacerImpl spacer : rowIndexToSpacer + .tailMap(changedRowIndex, false).values()) { + spacer.setPositionDiff(0, diffPx); + } + } + + /** + * Shifts spacers at and after a specific row by an amount of rows. + * <p> + * This moves both their associated row index and also their visual + * placement. + * <p> + * <em>Note:</em> This method does not check for the validity of any + * arguments. + * + * @param index + * the index of first row to move + * @param numberOfRows + * the number of rows to shift the spacers with. A positive + * value is downwards, a negative value is upwards. + */ + public void shiftSpacersByRows(int index, int numberOfRows) { + final double pxDiff = numberOfRows * body.getDefaultRowHeight(); + for (SpacerContainer.SpacerImpl spacer : getSpacersForRowAndAfter( + index)) { + spacer.setPositionDiff(0, pxDiff); + spacer.setRowIndex(spacer.getRow() + numberOfRows); + } + } + + private void updateSpacerDecosVisibility() { + final Range visibleRowRange = getVisibleRowRange(); + Collection<SpacerImpl> visibleSpacers = rowIndexToSpacer + .subMap(visibleRowRange.getStart() - 1, + visibleRowRange.getEnd() + 1) + .values(); + if (!visibleSpacers.isEmpty()) { + final double top = tableWrapper.getAbsoluteTop() + + header.getHeightOfSection(); + final double bottom = tableWrapper.getAbsoluteBottom() + - footer.getHeightOfSection(); + for (SpacerImpl spacer : visibleSpacers) { + spacer.updateDecoClip(top, bottom, spacerDecoWidth); + } + } + } + } + + private class ElementPositionBookkeeper { + /** + * A map containing cached values of an element's current top position. + */ + private final Map<Element, Double> elementTopPositionMap = new HashMap<Element, Double>(); + private final Map<Element, Double> elementLeftPositionMap = new HashMap<Element, Double>(); + + public void set(final Element e, final double x, final double y) { + assert e != null : "Element was null"; + position.set(e, x, y); + elementTopPositionMap.put(e, Double.valueOf(y)); + elementLeftPositionMap.put(e, Double.valueOf(x)); + } + + public double getTop(final Element e) { + Double top = elementTopPositionMap.get(e); + if (top == null) { + throw new IllegalArgumentException("Element " + e + + " was not found in the position bookkeeping"); + } + return top.doubleValue(); + } + + public double getLeft(final Element e) { + Double left = elementLeftPositionMap.get(e); + if (left == null) { + throw new IllegalArgumentException("Element " + e + + " was not found in the position bookkeeping"); + } + return left.doubleValue(); + } + + public void remove(Element e) { + elementTopPositionMap.remove(e); + elementLeftPositionMap.remove(e); + } + } + + /** + * Utility class for parsing and storing SubPart request string attributes + * for Grid and Escalator. + * + * @since 7.5.0 + */ + public static class SubPartArguments { + private String type; + private int[] indices; + + private SubPartArguments(String type, int[] indices) { + /* + * The constructor is private so that no third party would by + * mistake start using this parsing scheme, since it's not official + * by TestBench (yet?). + */ + + this.type = type; + this.indices = indices; + } + + public String getType() { + return type; + } + + public int getIndicesLength() { + return indices.length; + } + + public int getIndex(int i) { + return indices[i]; + } + + public int[] getIndices() { + return Arrays.copyOf(indices, indices.length); + } + + static SubPartArguments create(String subPart) { + String[] splitArgs = subPart.split("\\["); + String type = splitArgs[0]; + int[] indices = new int[splitArgs.length - 1]; + for (int i = 0; i < indices.length; ++i) { + String tmp = splitArgs[i + 1]; + indices[i] = Integer + .parseInt(tmp.substring(0, tmp.indexOf("]", 1))); + } + return new SubPartArguments(type, indices); + } + } + + // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y + /** + * The solution to + * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 30</code> + * . + * <p> + * This constant is placed in the Escalator class, instead of an inner + * class, since even mathematical expressions aren't allowed in non-static + * inner classes for constants. + */ + private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3); + /** + * The solution to + * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 40</code> + * . + * <p> + * This constant is placed in the Escalator class, instead of an inner + * class, since even mathematical expressions aren't allowed in non-static + * inner classes for constants. + */ + private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9); + + private static final String DEFAULT_WIDTH = "500.0px"; + private static final String DEFAULT_HEIGHT = "400.0px"; + + private FlyweightRow flyweightRow = new FlyweightRow(); + + /** The {@code <thead/>} tag. */ + private final TableSectionElement headElem = TableSectionElement + .as(DOM.createTHead()); + /** The {@code <tbody/>} tag. */ + private final TableSectionElement bodyElem = TableSectionElement + .as(DOM.createTBody()); + /** The {@code <tfoot/>} tag. */ + private final TableSectionElement footElem = TableSectionElement + .as(DOM.createTFoot()); + + /** + * TODO: investigate whether this field is now unnecessary, as + * {@link ScrollbarBundle} now caches its values. + * + * @deprecated maybe... + */ + @Deprecated + private double tBodyScrollTop = 0; + + /** + * TODO: investigate whether this field is now unnecessary, as + * {@link ScrollbarBundle} now caches its values. + * + * @deprecated maybe... + */ + @Deprecated + private double tBodyScrollLeft = 0; + + private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle(); + private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle(); + + private final HeaderRowContainer header = new HeaderRowContainer(headElem); + private final BodyRowContainerImpl body = new BodyRowContainerImpl( + bodyElem); + private final FooterRowContainer footer = new FooterRowContainer(footElem); + + /** + * 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 ≤ 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; + } +} |