diff options
author | Henrik Paul <henrik@vaadin.com> | 2014-06-10 21:50:51 +0300 |
---|---|---|
committer | Henrik Paul <henrik@vaadin.com> | 2014-06-27 12:39:42 +0300 |
commit | c4a1ee8a4fbc3fafaabea695d8aaf40aecbeba48 (patch) | |
tree | 9bbb9bef83f0e70309067e6d69adde7a7ee5a32a | |
parent | 51718c646883f6a9ca26a315d04de6d49119d492 (diff) | |
download | vaadin-framework-c4a1ee8a4fbc3fafaabea695d8aaf40aecbeba48.tar.gz vaadin-framework-c4a1ee8a4fbc3fafaabea695d8aaf40aecbeba48.zip |
Send selection between server and client (#13334)
Change-Id: I75174af63092fca72d9aa63ccf3c06a77f42c4f6
21 files changed, 850 insertions, 126 deletions
diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss index 6a050405cb..d1875a7ab3 100644 --- a/WebContent/VAADIN/themes/base/grid/grid.scss +++ b/WebContent/VAADIN/themes/base/grid/grid.scss @@ -14,4 +14,8 @@ } } -}
\ No newline at end of file + + .#{$primaryStyleName}-row-selected > td { + background: lightblue; + } +} diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index 2395dc848c..d6a609a3c8 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -42,7 +42,7 @@ import com.vaadin.shared.ui.grid.Range; */ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { - private class RowHandleImpl extends RowHandle<T> { + protected class RowHandleImpl extends RowHandle<T> { private T row; private final Object key; diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java index 2b9bf5c90e..3761ea92df 100644 --- a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java +++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONParser; +import com.google.gwt.json.client.JSONString; import com.google.gwt.json.client.JSONValue; import com.vaadin.client.ServerConnector; import com.vaadin.client.extensions.AbstractExtensionConnector; @@ -29,6 +30,7 @@ import com.vaadin.shared.data.DataProviderRpc; import com.vaadin.shared.data.DataProviderState; import com.vaadin.shared.data.DataRequestRpc; import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.Range; /** @@ -43,7 +45,8 @@ import com.vaadin.shared.ui.grid.Range; @Connect(com.vaadin.data.RpcDataProviderExtension.class) public class RpcDataSourceConnector extends AbstractExtensionConnector { - private final AbstractRemoteDataSource<JSONObject> dataSource = new AbstractRemoteDataSource<JSONObject>() { + public class RpcDataSource extends AbstractRemoteDataSource<JSONObject> { + @Override protected void requestRows(int firstRowIndex, int numberOfRows) { Range cached = getCachedRange(); @@ -54,18 +57,25 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { @Override public Object getRowKey(JSONObject row) { - /* - * FIXME will be properly implemented by another patch (Henrik Paul: - * 16.6.2014) - */ - return row; + JSONString string = row.get(GridState.JSONKEY_ROWKEY).isString(); + if (string != null) { + return string.stringValue(); + } else { + return null; + } + } + + public RowHandle<JSONObject> getHandleByKey(Object key) { + return new RowHandleImpl(null, key); } - }; + } + + private final RpcDataSource dataSource = new RpcDataSource(); @Override protected void extend(ServerConnector target) { dataSource.setEstimatedSize(getState().containerSize); - ((GridConnector) target).getWidget().setDataSource(dataSource); + ((GridConnector) target).setDataSource(dataSource); registerRpc(DataProviderRpc.class, new DataProviderRpc() { @Override diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java index 8a1f6f5842..c8feb6d18e 100644 --- a/client/src/com/vaadin/client/ui/grid/Escalator.java +++ b/client/src/com/vaadin/client/ui/grid/Escalator.java @@ -2678,6 +2678,9 @@ public class Escalator extends Widget { @Override protected void paintRemoveRows(final int index, final int numberOfRows) { + if (numberOfRows == 0) { + return; + } final Range viewportRange = Range.withLength( getLogicalRowIndex(visualRowOrder.getFirst()), diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index b5461e4a3b..9a75b37c42 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -48,7 +48,6 @@ import com.vaadin.client.ui.grid.renderers.ComplexRenderer; import com.vaadin.client.ui.grid.renderers.TextRenderer; import com.vaadin.client.ui.grid.renderers.WidgetRenderer; import com.vaadin.client.ui.grid.selection.HasSelectionChangeHandlers; -import com.vaadin.client.ui.grid.selection.MultiSelectionRenderer; import com.vaadin.client.ui.grid.selection.SelectionChangeEvent; import com.vaadin.client.ui.grid.selection.SelectionChangeHandler; import com.vaadin.client.ui.grid.selection.SelectionModel; @@ -1061,7 +1060,7 @@ public class Grid<T> extends Composite implements refreshHeader(); refreshFooter(); - selectionModel = SelectionMode.SINGLE.createModel(); + setSelectionMode(SelectionMode.SINGLE); escalator .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() { @@ -1075,6 +1074,16 @@ public class Grid<T> extends Composite implements } } }); + + // Default action on SelectionChangeEvents. Refresh the body so changed + // become visible. + addSelectionChangeHandler(new SelectionChangeHandler() { + + @Override + public void onSelectionChange(SelectionChangeEvent<?> event) { + refreshBody(); + } + }); } @Override @@ -1341,6 +1350,13 @@ public class Grid<T> extends Composite implements } /** + * Refreshes all body rows + */ + private void refreshBody() { + escalator.getBody().refreshRows(0, escalator.getBody().getRowCount()); + } + + /** * Refreshes all footer rows */ void refreshFooter() { @@ -1797,6 +1813,15 @@ public class Grid<T> extends Composite implements } /** + * Gets the {@Link DataSource} for this Grid. + * + * @return the data source used by this grid + */ + public DataSource<T> getDataSource() { + return dataSource; + } + + /** * Sets the rightmost frozen column in the grid. * <p> * All columns up to and including the given column will be frozen in place @@ -2177,7 +2202,7 @@ public class Grid<T> extends Composite implements /* TODO remove before final */ public void setSelectionCheckboxes(boolean set) { if (set) { - setSelectColumnRenderer(new MultiSelectionRenderer(this)); + setSelectColumnRenderer(selectionModel.getSelectionColumnRenderer()); } else { setSelectColumnRenderer(null); } @@ -2198,6 +2223,8 @@ public class Grid<T> extends Composite implements /** * Sets the current selection model. + * <p> + * This function will call {@link SelectionModel#setGrid(Grid)}. * * @param selectionModel * a selection model implementation. @@ -2211,6 +2238,7 @@ public class Grid<T> extends Composite implements } this.selectionModel = selectionModel; + selectionModel.setGrid(this); } @@ -2412,14 +2440,4 @@ public class Grid<T> extends Composite implements fireEvent(new SortEvent<T>(this, Collections.unmodifiableList(sortOrder))); } - - /** - * Missing getDataSource method. TODO: remove this and other duplicates - * after The Merge - * - * @return a DataSource reference - */ - public DataSource<T> getDataSource() { - return dataSource; - } } diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java index 0bfcf8ffcd..3b1ecb44d8 100644 --- a/client/src/com/vaadin/client/ui/grid/GridConnector.java +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -17,9 +17,11 @@ package com.vaadin.client.ui.grid; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -28,13 +30,18 @@ import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONValue; import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.data.RpcDataSourceConnector.RpcDataSource; import com.vaadin.client.ui.AbstractComponentConnector; import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector; +import com.vaadin.client.ui.grid.selection.SelectionChangeEvent; +import com.vaadin.client.ui.grid.selection.SelectionChangeHandler; +import com.vaadin.client.ui.grid.selection.SelectionModelMulti; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.grid.ColumnGroupRowState; import com.vaadin.shared.ui.grid.ColumnGroupState; import com.vaadin.shared.ui.grid.GridClientRpc; import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.ScrollDestination; @@ -53,7 +60,66 @@ import com.vaadin.shared.ui.grid.ScrollDestination; public class GridConnector extends AbstractComponentConnector { /** - * Custom implementation of the custom grid column using a String[] to + * Hacked SelectionModelMulti to make selection communication work for now. + */ + private class RowKeyBasedMultiSelection extends + SelectionModelMulti<JSONObject> { + + private final LinkedHashSet<String> selectedKeys = new LinkedHashSet<String>(); + + public List<String> getSelectedKeys() { + List<String> keys = new ArrayList<String>(); + keys.addAll(selectedKeys); + return keys; + } + + public void updateFromState() { + boolean changed = false; + Set<String> stateKeys = new LinkedHashSet<String>(); + stateKeys.addAll(getState().selectedKeys); + for (String key : stateKeys) { + if (!selectedKeys.contains(key)) { + changed = true; + selectByHandle(dataSource.getHandleByKey(key)); + } + } + for (String key : selectedKeys) { + changed = true; + if (!stateKeys.contains(key)) { + deselectByHandle(dataSource.getHandleByKey(key)); + } + } + selectedKeys.clear(); + selectedKeys.addAll(stateKeys); + + if (changed) { + // At least for now there's no way to send the selected and/or + // deselected row data. Some data is only stored as keys + getWidget().fireEvent( + new SelectionChangeEvent<JSONObject>(getWidget(), + (List<JSONObject>) null, null)); + } + } + + @Override + public boolean select(Collection<JSONObject> rows) { + for (JSONObject row : rows) { + selectedKeys.add((String) dataSource.getRowKey(row)); + } + return super.select(rows); + } + + @Override + public boolean deselect(Collection<JSONObject> rows) { + for (JSONObject row : rows) { + selectedKeys.remove(dataSource.getRowKey(row)); + } + return super.deselect(rows); + } + } + + /** + * Custom implementation of the custom grid column using a JSONObject to * represent the cell value and String as a column type. */ private class CustomGridColumn extends GridColumn<Object, JSONObject> { @@ -107,6 +173,8 @@ public class GridConnector extends AbstractComponentConnector { * Maps a generated column id to a grid column instance */ private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>(); + private final RowKeyBasedMultiSelection selectionModel = new RowKeyBasedMultiSelection(); + private RpcDataSource dataSource; @Override @SuppressWarnings("unchecked") @@ -139,6 +207,18 @@ public class GridConnector extends AbstractComponentConnector { getWidget().scrollToRow(row, destination); } }); + + getWidget().setSelectionModel(selectionModel); + + getWidget().addSelectionChangeHandler(new SelectionChangeHandler() { + @Override + public void onSelectionChange(SelectionChangeEvent<?> event) { + // TODO change this to diff based. (henrik paul 24.6.2014) + getRpcProxy(GridServerRpc.class).selectionChange( + selectionModel.getSelectedKeys()); + } + }); + } @Override @@ -211,6 +291,10 @@ public class GridConnector extends AbstractComponentConnector { if (stateChangeEvent.hasPropertyChanged("heightMode")) { getWidget().setHeightMode(getState().heightMode); } + + if (stateChangeEvent.hasPropertyChanged("selectedKeys")) { + selectionModel.updateFromState(); + } } /** @@ -332,4 +416,9 @@ public class GridConnector extends AbstractComponentConnector { } } } + + public void setDataSource(RpcDataSource dataSource) { + this.dataSource = dataSource; + getWidget().setDataSource(this.dataSource); + } } diff --git a/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java b/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java index 52bb6c0f60..53b0d064ab 100644 --- a/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java +++ b/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java @@ -17,7 +17,6 @@ package com.vaadin.client.ui.grid.selection; import java.util.Collection; import java.util.HashSet; -import java.util.logging.Logger; import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.Element; @@ -37,7 +36,7 @@ import com.vaadin.client.ui.grid.Grid; import com.vaadin.client.ui.grid.renderers.ComplexRenderer; /* This class will probably not survive the final merge of all selection functionality. */ -public class MultiSelectionRenderer extends ComplexRenderer<Boolean> { +public class MultiSelectionRenderer<T> extends ComplexRenderer<Boolean> { private class TouchEventHandler implements NativePreviewHandler { @Override @@ -168,12 +167,12 @@ public class MultiSelectionRenderer extends ComplexRenderer<Boolean> { private static final String LOGICAL_ROW_PROPERTY_INT = "vEscalatorLogicalRow"; - private final Grid<?> grid; + private final Grid<T> grid; private HandlerRegistration nativePreviewHandlerRegistration; private final SelectionHandler selectionHandler = new SelectionHandler(); - public MultiSelectionRenderer(final Grid<?> grid) { + public MultiSelectionRenderer(final Grid<T> grid) { this.grid = grid; } @@ -276,23 +275,16 @@ public class MultiSelectionRenderer extends ComplexRenderer<Boolean> { } } - private boolean isSelected(final int logicalRow) { - // TODO - // return grid.getSelectionModel().isSelected(logicalRow); - return false; + protected boolean isSelected(final int logicalRow) { + return grid.isSelected(grid.getDataSource().getRow(logicalRow)); } - private void setSelected(final int logicalRow, final boolean select) { + protected void setSelected(final int logicalRow, final boolean select) { + T row = grid.getDataSource().getRow(logicalRow); if (select) { - // TODO - // grid.getSelectionModel().select(logicalRow); - Logger.getLogger(getClass().getName()).warning( - "Selecting " + logicalRow); + grid.select(row); } else { - // TODO - // grid.getSelectionModel().deselect(logicalRow); - Logger.getLogger(getClass().getName()).warning( - "Deselecting " + logicalRow); + grid.deselect(row); } } } diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java index d11b7764d0..989a8946c7 100644 --- a/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java @@ -25,7 +25,7 @@ import com.vaadin.client.ui.grid.Renderer; * <p> * Selection models perform tracking of selected rows in the Grid, as well as * dispatching events when the selection state changes. - * + * * @author Vaadin Ltd * @since 7.4 * @param <T> @@ -36,7 +36,7 @@ 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 @@ -47,18 +47,18 @@ public interface SelectionModel<T> { /** * 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<T> getSelectionColumnRenderer(); + 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 */ @@ -74,7 +74,7 @@ public interface SelectionModel<T> { /** * Returns a Collection containing all selected rows. - * + * * @return a non-null collection. */ public Collection<T> getSelectedRows(); @@ -82,7 +82,7 @@ public interface SelectionModel<T> { /** * 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 */ @@ -90,7 +90,7 @@ public interface SelectionModel<T> { /** * Selects a row. - * + * * @param row * a {@link Grid} row object * @return true, if this row as not previously selected. @@ -101,7 +101,7 @@ public interface SelectionModel<T> { * 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. @@ -110,7 +110,7 @@ public interface SelectionModel<T> { /** * Returns the currently selected row. - * + * * @return a {@link Grid} row object or null, if nothing is selected. */ public T getSelectedRow(); @@ -119,7 +119,7 @@ public interface SelectionModel<T> { /** * Selection model that allows for several rows to be selected at once. - * + * * @param <T> * type parameter corresponding with Grid row type */ @@ -127,7 +127,7 @@ public interface SelectionModel<T> { /** * Selects one or more rows. - * + * * @param rows * {@link Grid} row objects * @return true, if the set of selected rows was changed. @@ -136,7 +136,7 @@ public interface SelectionModel<T> { /** * Deselects one or more rows. - * + * * @param rows * Grid row objects * @return true, if the set of selected rows was changed. @@ -145,14 +145,14 @@ public interface SelectionModel<T> { /** * 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. @@ -161,7 +161,7 @@ public interface SelectionModel<T> { /** * 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. @@ -173,7 +173,7 @@ public interface SelectionModel<T> { /** * Interface for a selection model that does not allow anything to be * selected. - * + * * @param <T> * type parameter corresponding with Grid row type */ diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java index 8afb592771..de62dc9cbc 100644 --- a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java @@ -17,38 +17,38 @@ package com.vaadin.client.ui.grid.selection; 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.client.ui.grid.Grid; import com.vaadin.client.ui.grid.Renderer; /** * Multi-row selection model. - * + * * @author Vaadin Ltd * @since 7.4 */ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> { - private final Renderer<T> renderer; - private final Set<T> selectedRows; + private final Set<RowHandle<T>> selectedRows; + private Renderer<Boolean> renderer; private Grid<T> grid; public SelectionModelMulti() { grid = null; renderer = null; - selectedRows = new LinkedHashSet<T>(); + selectedRows = new LinkedHashSet<RowHandle<T>>(); } @Override public boolean isSelected(T row) { - return selectedRows.contains(row); + return isSelectedByHandle(grid.getDataSource().getHandle(row)); } @Override - public Renderer<T> getSelectionColumnRenderer() { + public Renderer<Boolean> getSelectionColumnRenderer() { return renderer; } @@ -64,6 +64,8 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> { throw new IllegalStateException( "Grid reference cannot be reassigned"); } + + this.renderer = new MultiSelectionRenderer<T>(grid); } @Override @@ -87,7 +89,7 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> { if (selectedRows.size() > 0) { SelectionChangeEvent<T> event = new SelectionChangeEvent<T>(grid, - null, selectedRows); + null, getSelectedRows()); selectedRows.clear(); grid.fireEvent(event); @@ -105,7 +107,8 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> { Set<T> added = new LinkedHashSet<T>(); for (T row : rows) { - if (selectedRows.add(row)) { + RowHandle<T> handle = grid.getDataSource().getHandle(row); + if (selectByHandle(handle)) { added.add(row); } } @@ -127,7 +130,7 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> { Set<T> removed = new LinkedHashSet<T>(); for (T row : rows) { - if (selectedRows.remove(row)) { + if (deselectByHandle(grid.getDataSource().getHandle(row))) { removed.add(row); } } @@ -140,14 +143,37 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> { return false; } + protected boolean isSelectedByHandle(RowHandle<T> handle) { + return selectedRows.contains(handle); + } + + protected boolean selectByHandle(RowHandle<T> handle) { + if (selectedRows.add(handle)) { + handle.pin(); + return true; + } + return false; + } + + protected boolean deselectByHandle(RowHandle<T> handle) { + if (selectedRows.remove(handle)) { + handle.unpin(); + return true; + } + return false; + } + @Override public Collection<T> getSelectedRows() { - return Collections.unmodifiableSet(selectedRows); + Set<T> selected = new LinkedHashSet<T>(); + for (RowHandle<T> handle : selectedRows) { + selected.add(handle.getRow()); + } + return selected; } @Override public void reset() { deselectAll(); } - } diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java index bcb0357089..93dfb49df2 100644 --- a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java @@ -23,7 +23,7 @@ import com.vaadin.client.ui.grid.Renderer; /** * No-row selection model. - * + * * @author Vaadin Ltd * @since 7.4 */ @@ -35,7 +35,7 @@ public class SelectionModelNone<T> implements SelectionModel.None<T> { } @Override - public Renderer<T> getSelectionColumnRenderer() { + public Renderer<Boolean> getSelectionColumnRenderer() { return null; } diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java index 6b5f645e23..775e1878c5 100644 --- a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java @@ -18,30 +18,31 @@ package com.vaadin.client.ui.grid.selection; import java.util.Collection; import java.util.Collections; +import com.vaadin.client.data.DataSource.RowHandle; import com.vaadin.client.ui.grid.Grid; import com.vaadin.client.ui.grid.Renderer; /** * Single-row selection model. - * + * * @author Vaadin Ltd * @since 7.4 */ public class SelectionModelSingle<T> implements SelectionModel.Single<T> { private Grid<T> grid; - private T selectedRow; + private RowHandle<T> selectedRow; + private Renderer<Boolean> renderer; @Override public boolean isSelected(T row) { - return row == null ? null : row.equals(getSelectedRow()); + return selectedRow != null + && selectedRow.equals(grid.getDataSource().getHandle(row)); } @Override - public Renderer<T> getSelectionColumnRenderer() { - // TODO: Add implementation of SelectionColumnRenderer; currently none - // exists - return null; + public Renderer<Boolean> getSelectionColumnRenderer() { + return renderer; } @Override @@ -56,6 +57,7 @@ public class SelectionModelSingle<T> implements SelectionModel.Single<T> { throw new IllegalStateException( "Grid reference cannot be reassigned"); } + renderer = new MultiSelectionRenderer<T>(grid); } @Override @@ -65,12 +67,17 @@ public class SelectionModelSingle<T> implements SelectionModel.Single<T> { throw new IllegalArgumentException("Row cannot be null"); } - if (row.equals(getSelectedRow())) { + if (isSelected(row)) { return false; } - T removed = selectedRow; - selectedRow = row; + T removed = getSelectedRow(); + if (selectedRow != null) { + selectedRow.unpin(); + } + selectedRow = grid.getDataSource().getHandle(row); + selectedRow.pin(); + grid.fireEvent(new SelectionChangeEvent<T>(grid, row, removed)); return true; @@ -83,8 +90,9 @@ public class SelectionModelSingle<T> implements SelectionModel.Single<T> { throw new IllegalArgumentException("Row cannot be null"); } - if (row.equals(selectedRow)) { - T removed = selectedRow; + if (isSelected(row)) { + T removed = selectedRow.getRow(); + selectedRow.unpin(); selectedRow = null; grid.fireEvent(new SelectionChangeEvent<T>(grid, null, removed)); return true; @@ -95,16 +103,15 @@ public class SelectionModelSingle<T> implements SelectionModel.Single<T> { @Override public T getSelectedRow() { - return selectedRow; + return (selectedRow != null ? selectedRow.getRow() : null); } @Override public void reset() { - T removed = selectedRow; - selectedRow = null; + T removed = getSelectedRow(); if (removed != null) { - grid.fireEvent(new SelectionChangeEvent<T>(grid, null, removed)); + deselect(removed); } } diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index 0046b256bb..1834822d99 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -17,6 +17,7 @@ package com.vaadin.data; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -24,11 +25,14 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import com.google.gwt.thirdparty.guava.common.collect.BiMap; +import com.google.gwt.thirdparty.guava.common.collect.HashBiMap; import com.vaadin.data.Container.Indexed; import com.vaadin.data.Container.Indexed.ItemAddEvent; import com.vaadin.data.Container.Indexed.ItemRemoveEvent; @@ -63,6 +67,227 @@ import com.vaadin.ui.components.grid.Renderer; public class RpcDataProviderExtension extends AbstractExtension { /** + * ItemId to Key to ItemId mapper. + * <p> + * This class is used when transmitting information about items in container + * related to Grid. It introduces a consistent way of mapping ItemIds and + * its container to a String that can be mapped back to ItemId. + * <p> + * <em>Technical note:</em> This class also keeps tabs on which indices are + * being shown/selected, and is able to clean up after itself once the + * itemId ⇆ key mapping is not needed anymore. In other words, this + * doesn't leak memory. + */ + public class DataProviderKeyMapper { + private final BiMap<Integer, Object> indexToItemId = HashBiMap.create(); + private final BiMap<Object, String> itemIdToKey = HashBiMap.create(); + private Set<Object> pinnedItemIds = new HashSet<Object>(); + private Range activeRange = Range.withLength(0, 0); + private long rollingIndex = 0; + + private DataProviderKeyMapper() { + // private implementation + } + + void preActiveRowsChange(Range newActiveRange, int firstNewIndex, + List<?> itemIds) { + final Range[] removed = activeRange.partitionWith(newActiveRange); + final Range[] added = newActiveRange.partitionWith(activeRange); + + removeActiveRows(removed[0]); + removeActiveRows(removed[2]); + addActiveRows(added[0], firstNewIndex, itemIds); + addActiveRows(added[2], firstNewIndex, itemIds); + + activeRange = newActiveRange; + } + + private void removeActiveRows(final Range deprecated) { + for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) { + final Integer ii = Integer.valueOf(i); + final Object itemId = indexToItemId.get(ii); + + if (!pinnedItemIds.contains(itemId)) { + itemIdToKey.remove(itemId); + } + indexToItemId.remove(ii); + } + } + + private void addActiveRows(final Range added, int firstNewIndex, + List<?> newItemIds) { + + for (int i = added.getStart(); i < added.getEnd(); i++) { + + /* + * We might be in a situation we have an index <-> itemId entry + * already. This happens when something was selected, scrolled + * out of view and now we're scrolling it back into view. It's + * unnecessary to overwrite it in that case. + * + * Fun thought: considering branch prediction, it _might_ even + * be a bit faster to simply always run the code beyond this + * if-state. But it sounds too stupid (and most often too + * insignificant) to try out. + */ + final Integer ii = Integer.valueOf(i); + if (indexToItemId.containsKey(ii)) { + continue; + } + + /* + * We might be in a situation where we have an itemId <-> key + * entry already, but no index for it. This happens when + * something that is out of view is selected programmatically. + * In that case, we only want to add an index for that entry, + * and not overwrite the key. + */ + final Object itemId = newItemIds.get(i - firstNewIndex); + if (!itemIdToKey.containsKey(itemId)) { + itemIdToKey.put(itemId, nextKey()); + } + indexToItemId.put(ii, itemId); + } + } + + private String nextKey() { + return String.valueOf(rollingIndex++); + } + + String getKey(Object itemId) { + String key = itemIdToKey.get(itemId); + if (key == null) { + key = nextKey(); + itemIdToKey.put(itemId, key); + } + return key; + } + + /** + * Gets keys for a collection of item ids. + * <p> + * If the itemIds are currently cached, the existing keys will be used. + * Otherwise new ones will be created. + * + * @param itemIds + * the item ids for which to get keys + * @return keys for the {@code itemIds} + */ + public List<String> getKeys(Collection<Object> itemIds) { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds can't be null"); + } + + ArrayList<String> keys = new ArrayList<String>(itemIds.size()); + for (Object itemId : itemIds) { + keys.add(getKey(itemId)); + } + return keys; + } + + Object getItemId(String key) throws IllegalStateException { + Object itemId = itemIdToKey.inverse().get(key); + if (itemId != null) { + return itemId; + } else { + throw new IllegalStateException("No item id for key " + key + + " found."); + } + } + + /** + * Gets corresponding item ids for each of the keys in a collection. + * + * @param keys + * the keys for which to retrieve item ids + * @return a collection of item ids for the {@code keys} + * @throws IllegalStateException + * if one or more of keys don't have a corresponding item id + * in the cache + */ + public Collection<Object> getItemIds(Collection<String> keys) + throws IllegalStateException { + if (keys == null) { + throw new IllegalArgumentException("keys may not be null"); + } + + ArrayList<Object> itemIds = new ArrayList<Object>(keys.size()); + for (String key : keys) { + itemIds.add(getItemId(key)); + } + return itemIds; + } + + /** + * Pin an item id to be cached indefinitely. + * <p> + * Normally when an itemId is not an active row, it is discarded from + * the cache. Pinning an item id will make sure that it is kept in the + * cache. + * <p> + * In effect, while an item id is pinned, it always has the same key. + * + * @param itemId + * the item id to pin + * @throws IllegalStateException + * if {@code itemId} was already pinned + * @see #unpin(Object) + * @see #isPinned(Object) + * @see #getItemIds(Collection) + */ + public void pin(Object itemId) throws IllegalStateException { + if (isPinned(itemId)) { + throw new IllegalStateException("Item id " + itemId + + " was pinned already"); + } + pinnedItemIds.add(itemId); + } + + /** + * Unpin an item id. + * <p> + * This cancels the effect of pinning an item id. If the item id is + * currently inactive, it will be immediately removed from the cache. + * + * @param itemId + * the item id to unpin + * @throws IllegalStateException + * if {@code itemId} was not pinned + * @see #pin(Object) + * @see #isPinned(Object) + * @see #getItemIds(Collection) + */ + public void unpin(Object itemId) throws IllegalStateException { + if (!isPinned(itemId)) { + throw new IllegalStateException("Item id " + itemId + + " was not pinned"); + } + + pinnedItemIds.remove(itemId); + final Integer removedIndex = indexToItemId.inverse().remove(itemId); + if (removedIndex == null + || !activeRange.contains(removedIndex.intValue())) { + itemIdToKey.remove(itemId); + } + } + + /** + * Checks whether an item id is pinned or not. + * + * @param itemId + * the item id to check for pin status + * @return {@code true} iff the item id is currently pinned + */ + public boolean isPinned(Object itemId) { + return pinnedItemIds.contains(itemId); + } + + Object itemIdAtIndex(int index) { + return indexToItemId.inverse().get(Integer.valueOf(index)); + } + } + + /** * A helper class that handles the client-side Escalator logic relating to * making sure that whatever is currently visible to the user, is properly * initialized and otherwise handled on the server side (as far as @@ -70,8 +295,9 @@ public class RpcDataProviderExtension extends AbstractExtension { * <p> * This bookeeping includes, but is not limited to: * <ul> - * <li>listening to the currently visible {@link Property Properties'} value - * changes on the server side and sending those back to the client; and + * <li>listening to the currently visible {@link com.vaadin.data.Property + * Properties'} value changes on the server side and sending those back to + * the client; and * <li>attaching and detaching {@link com.vaadin.ui.Component Components} * from the Vaadin Component hierarchy. * </ul> @@ -340,7 +566,7 @@ public class RpcDataProviderExtension extends AbstractExtension { ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; int firstIndex = removeEvent.getFirstIndex(); int count = removeEvent.getRemovedItemsCount(); - removeRowData(firstIndex, count, removeEvent.getFirstItemId()); + removeRowData(firstIndex, count); } else { @@ -353,6 +579,8 @@ public class RpcDataProviderExtension extends AbstractExtension { } }; + private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper(); + /** * Creates a new data provider using the given container. * @@ -366,8 +594,6 @@ public class RpcDataProviderExtension extends AbstractExtension { @Override public void requestRows(int firstRow, int numberOfRows, int firstCachedRowIndex, int cacheSize) { - pushRows(firstRow, numberOfRows); - Range active = Range.withLength(firstRow, numberOfRows); if (cacheSize != 0) { Range cached = Range.withLength(firstCachedRowIndex, @@ -375,6 +601,11 @@ public class RpcDataProviderExtension extends AbstractExtension { active = active.combineWith(cached); } + List<?> itemIds = RpcDataProviderExtension.this.container + .getItemIds(firstRow, numberOfRows); + keyMapper.preActiveRowsChange(active, firstRow, itemIds); + pushRows(firstRow, itemIds); + activeRowHandler.setActiveRows(active.getStart(), active.length()); } @@ -389,8 +620,7 @@ public class RpcDataProviderExtension extends AbstractExtension { } - private void pushRows(int firstRow, int numberOfRows) { - List<?> itemIds = container.getItemIds(firstRow, numberOfRows); + private void pushRows(int firstRow, List<?> itemIds) { Collection<?> propertyIds = container.getContainerPropertyIds(); JSONArray rows = new JSONArray(); for (Object itemId : itemIds) { @@ -402,6 +632,7 @@ public class RpcDataProviderExtension extends AbstractExtension { private JSONObject getRowData(Collection<?> propertyIds, Object itemId) { Item item = container.getItem(itemId); + String[] row = new String[propertyIds.size()]; JSONArray rowData = new JSONArray(); @@ -421,13 +652,7 @@ public class RpcDataProviderExtension extends AbstractExtension { final JSONObject rowObject = new JSONObject(); rowObject.put(GridState.JSONKEY_DATA, rowData); - /* - * TODO: selection wants to put here something in the lines of: - * - * rowObject.put(GridState.JSONKEY_ROWKEY, getKey(itemId)) - * - * Henrik Paul: 18.6.2014 - */ + rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId)); return rowObject; } catch (final JSONException e) { throw new RuntimeException("Grid was unable to serialize " @@ -477,23 +702,16 @@ public class RpcDataProviderExtension extends AbstractExtension { * @param firstItemId * the item id of the first removed item */ - private void removeRowData(int firstIndex, int count, Object firstItemId) { + private void removeRowData(int firstIndex, int count) { getState().containerSize -= count; getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count); - /* - * Unfortunately, there's no sane way of getting the rest of the removed - * itemIds unless we cache a mapping between index and itemId. - * - * Fortunately, the only time _currently_ an event with more than one - * removed item seems to be when calling - * AbstractInMemoryContainer.removeAllElements(). Otherwise, it's only - * removing one item at a time. - * - * We _could_ have a backup of all the itemIds, and compare to that one, - * but we really really don't want to go there. - */ - activeRowHandler.removeItemId(firstItemId); + for (int i = 0; i < count; i++) { + Object itemId = keyMapper.itemIdAtIndex(firstIndex + i); + if (itemId != null) { + activeRowHandler.removeItemId(itemId); + } + } } /** @@ -566,6 +784,10 @@ public class RpcDataProviderExtension extends AbstractExtension { activeRowHandler.propertiesAdded(addedPropertyIds); } + public DataProviderKeyMapper getKeyMapper() { + return keyMapper; + } + protected Grid getGrid() { return (Grid) getParent(); } diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 1ebf227330..bc6a69e850 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -26,16 +26,23 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.json.JSONArray; +import org.json.JSONException; + +import com.google.gwt.thirdparty.guava.common.collect.Sets; +import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView; import com.vaadin.data.Container; import com.vaadin.data.Container.PropertySetChangeEvent; import com.vaadin.data.Container.PropertySetChangeListener; import com.vaadin.data.Container.PropertySetChangeNotifier; import com.vaadin.data.Container.Sortable; import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper; import com.vaadin.server.KeyMapper; import com.vaadin.shared.ui.grid.ColumnGroupRowState; import com.vaadin.shared.ui.grid.GridClientRpc; import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.ScrollDestination; @@ -181,6 +188,15 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier { */ private SelectionModel selectionModel; + /** + * The number of times to ignore selection state sync to the client. + * <p> + * This usually means that the client side has modified the selection. We + * still want to inform the listeners that the selection has changed, but we + * don't want to send those changes "back to the client". + */ + private int ignoreSelectionClientSync = 0; + private static final Method SELECTION_CHANGE_METHOD = ReflectTools .findMethod(SelectionChangeListener.class, "selectionChange", SelectionChangeEvent.class); @@ -191,9 +207,105 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier { * @param datasource * the data source for the grid */ - public Grid(Container.Indexed datasource) { + public Grid(final Container.Indexed datasource) { setContainerDataSource(datasource); setSelectionMode(SelectionMode.MULTI); + addSelectionChangeListener(new SelectionChangeListener() { + @Override + public void selectionChange(SelectionChangeEvent event) { + for (Object removedItemId : event.getRemoved()) { + keyMapper().unpin(removedItemId); + } + + for (Object addedItemId : event.getAdded()) { + keyMapper().pin(addedItemId); + } + + List<String> keys = keyMapper().getKeys(getSelectedRows()); + + boolean markAsDirty = true; + + /* + * If this clause is true, it means that the selection event + * originated from the client. This means that we don't want to + * send the changes back to the client (markAsDirty => false). + */ + if (ignoreSelectionClientSync > 0) { + ignoreSelectionClientSync--; + markAsDirty = false; + + try { + + /* + * Make sure that the diffstate is aware of the + * "undirty" modification, so that the diffs are + * calculated correctly the next time we actually want + * to send the selection state to the client. + */ + getUI().getConnectorTracker().getDiffState(Grid.this) + .put("selectedKeys", new JSONArray(keys)); + } catch (JSONException e) { + throw new RuntimeException("Internal error", e); + } + } + + getState(markAsDirty).selectedKeys = keys; + } + }); + + registerRpc(new GridServerRpc() { + + @Override + public void selectionChange(List<String> selection) { + final HashSet<Object> newSelection = new HashSet<Object>( + keyMapper().getItemIds(selection)); + final HashSet<Object> oldSelection = new HashSet<Object>( + getSelectedRows()); + + SetView<Object> addedItemIds = Sets.difference(newSelection, + oldSelection); + SetView<Object> removedItemIds = Sets.difference(oldSelection, + newSelection); + + if (!addedItemIds.isEmpty()) { + /* + * Since these changes come from the client, we want to + * modify the selection model and get that event fired to + * all the listeners. One of the listeners is our internal + * selection listener, and this tells it not to send the + * selection event back to the client. + */ + ignoreSelectionClientSync++; + + if (addedItemIds.size() == 1) { + select(addedItemIds.iterator().next()); + } else { + assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple selections, but the selection model is not a SelectionModel.Multi"; + ((SelectionModel.Multi) getSelectionModel()) + .select(addedItemIds); + } + } + + if (!removedItemIds.isEmpty()) { + /* + * Since these changes come from the client, we want to + * modify the selection model and get that event fired to + * all the listeners. One of the listeners is our internal + * selection listener, and this tells it not to send the + * selection event back to the client. + */ + ignoreSelectionClientSync++; + + if (removedItemIds.size() == 1) { + deselect(removedItemIds.iterator().next()); + } else { + assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple deselections, but the selection model is not a SelectionModel.Multi"; + ((SelectionModel.Multi) getSelectionModel()) + .deselect(removedItemIds); + } + } + } + }); } /** @@ -205,6 +317,7 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier { * if the data source is null */ public void setContainerDataSource(Container.Indexed container) { + if (container == null) { throw new IllegalArgumentException( "Cannot set the datasource to null"); @@ -935,12 +1048,22 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier { SELECTION_CHANGE_METHOD); } - /** FIXME remove once selection mode communcation is done. only for testing. */ + /** + * FIXME remove once selection mode communication is done. only for testing. + */ public void setSelectionCheckboxes(boolean value) { getState().selectionCheckboxes = value; } /** + * A shortcut for + * <code>{@link #datasourceExtension}.{@link com.vaadin.data.RpcDataProviderExtension#getKeyMapper() getKeyMapper()}</code> + */ + private DataProviderKeyMapper keyMapper() { + return datasourceExtension.getKeyMapper(); + } + + /** * Adds a renderer to this grid's connector hierarchy. * * @param renderer diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java index cecdca80df..f0e25405cc 100644 --- a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java @@ -17,7 +17,7 @@ package com.vaadin.ui.components.grid.selection; import java.util.Collection; import java.util.EventObject; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import com.google.gwt.thirdparty.guava.common.collect.Sets; @@ -32,14 +32,14 @@ import com.vaadin.ui.components.grid.Grid; */ public class SelectionChangeEvent extends EventObject { - private Set<Object> oldSelection; - private Set<Object> newSelection; + private LinkedHashSet<Object> oldSelection; + private LinkedHashSet<Object> newSelection; public SelectionChangeEvent(Grid source, Collection<Object> oldSelection, Collection<Object> newSelection) { super(source); - this.oldSelection = new HashSet<Object>(oldSelection); - this.newSelection = new HashSet<Object>(newSelection); + this.oldSelection = new LinkedHashSet<Object>(oldSelection); + this.newSelection = new LinkedHashSet<Object>(newSelection); } /** diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java new file mode 100644 index 0000000000..9ecf131c5b --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper; +import com.vaadin.data.util.IndexedContainer; + +public class DataProviderExtension { + private RpcDataProviderExtension dataProvider; + private DataProviderKeyMapper keyMapper; + private Container.Indexed container; + + private static final Object ITEM_ID1 = "itemid1"; + private static final Object ITEM_ID2 = "itemid2"; + private static final Object ITEM_ID3 = "itemid3"; + + private static final Object PROPERTY_ID1_STRING = "property1"; + + @Before + public void setup() { + container = new IndexedContainer(); + populate(container); + + dataProvider = new RpcDataProviderExtension(container); + keyMapper = dataProvider.getKeyMapper(); + } + + private static void populate(Indexed container) { + container.addContainerProperty(PROPERTY_ID1_STRING, String.class, ""); + for (Object itemId : Arrays.asList(ITEM_ID1, ITEM_ID2, ITEM_ID3)) { + final Item item = container.addItem(itemId); + @SuppressWarnings("unchecked") + final Property<String> stringProperty = item + .getItemProperty(PROPERTY_ID1_STRING); + stringProperty.setValue(itemId.toString()); + } + } + + @Test + public void pinBasics() { + assertFalse("itemId1 should not start as pinned", + keyMapper.isPinned(ITEM_ID2)); + + keyMapper.pin(ITEM_ID1); + assertTrue("itemId1 should now be pinned", keyMapper.isPinned(ITEM_ID1)); + + keyMapper.unpin(ITEM_ID1); + assertFalse("itemId1 should not be pinned anymore", + keyMapper.isPinned(ITEM_ID2)); + } + + @Test(expected = IllegalStateException.class) + public void doublePinning() { + keyMapper.pin(ITEM_ID1); + keyMapper.pin(ITEM_ID1); + } + + @Test(expected = IllegalStateException.class) + public void nonexistentUnpin() { + keyMapper.unpin(ITEM_ID1); + } +} diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java index a92ffe0421..43469914e5 100644 --- a/shared/src/com/vaadin/shared/data/DataProviderRpc.java +++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java @@ -34,7 +34,8 @@ public interface DataProviderRpc extends ClientRpc { * * <pre> * [{ - * "d": [COL_1_JSON, COL_2_json, ...] + * "d": [COL_1_JSON, COL_2_json, ...], + * "k": "1" * }, * ... * ] diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java new file mode 100644 index 0000000000..b763174e53 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java @@ -0,0 +1,30 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import java.util.List; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * Client-to-server RPC interface for the Grid component + * + * @since 7.4 + * @author Vaadin Ltd + */ +public interface GridServerRpc extends ServerRpc { + void selectionChange(List<String> newSelection); +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java index eceaedd1fc..0b23e2c11d 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridState.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -39,11 +39,18 @@ public class GridState extends AbstractComponentState { /** * The key in which a row's data can be found - * {@link com.vaadin.shared.data.DataProviderRpc#setRowData(int, List) - * DataProviderRpc.setRowData(int, List)} + * + * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String) */ public static final String JSONKEY_DATA = "d"; + /** + * The key in which a row's own key can be found + * + * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String) + */ + public static final String JSONKEY_ROWKEY = "k"; + { // FIXME Grid currently does not support undefined size width = "400px"; @@ -97,4 +104,7 @@ public class GridState extends AbstractComponentState { @DelegateToWidget public boolean selectionCheckboxes; + // instantiated just to avoid NPEs + public List<String> selectedKeys = new ArrayList<String>(); + } diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java index 06fe088dee..c6597ef23b 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -363,6 +363,20 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { } } }, null); + + createBooleanAction("Select first row", "Body rows", false, + new Command<Grid, Boolean>() { + @Override + public void execute(Grid grid, Boolean select, Object data) { + final Object firstItemId = grid + .getContainerDatasource().firstItemId(); + if (select.booleanValue()) { + grid.select(firstItemId); + } else { + grid.deselect(firstItemId); + } + } + }); } @SuppressWarnings("boxing") diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java index a11b0f1be9..3dc8ac814f 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java @@ -18,6 +18,7 @@ package com.vaadin.tests.components.grid; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.util.ArrayList; @@ -301,6 +302,83 @@ public class GridBasicFeaturesTest extends MultiBrowserTest { "modified: Column0", getBodyCellByRowAndColumn(0, 0).getText()); } + @Test + public void testSelectOnOff() throws Exception { + openTestURL(); + + assertFalse("row shouldn't start out as selected", + isSelected(getRow(0))); + toggleFirstRowSelection(); + assertTrue("row should become selected", isSelected(getRow(0))); + toggleFirstRowSelection(); + assertFalse("row shouldn't remain selected", isSelected(getRow(0))); + } + + @Test + public void testSelectOnScrollOffScroll() throws Exception { + openTestURL(); + assertFalse("row shouldn't start out as selected", + isSelected(getRow(0))); + toggleFirstRowSelection(); + assertTrue("row should become selected", isSelected(getRow(0))); + + scrollGridVerticallyTo(10000); // make sure the row is out of cache + scrollGridVerticallyTo(0); // scroll it back into view + + assertTrue("row should still be selected when scrolling " + + "back into view", isSelected(getRow(0))); + } + + @Test + public void testSelectScrollOnScrollOff() throws Exception { + openTestURL(); + assertFalse("row shouldn't start out as selected", + isSelected(getRow(0))); + + scrollGridVerticallyTo(10000); // make sure the row is out of cache + toggleFirstRowSelection(); + + scrollGridVerticallyTo(0); // scroll it back into view + assertTrue("row should still be selected when scrolling " + + "back into view", isSelected(getRow(0))); + + toggleFirstRowSelection(); + assertFalse("row shouldn't remain selected", isSelected(getRow(0))); + } + + @Test + public void testSelectScrollOnOffScroll() throws Exception { + openTestURL(); + assertFalse("row shouldn't start out as selected", + isSelected(getRow(0))); + + scrollGridVerticallyTo(10000); // make sure the row is out of cache + toggleFirstRowSelection(); + toggleFirstRowSelection(); + + scrollGridVerticallyTo(0); // make sure the row is out of cache + assertFalse("row shouldn't be selected when scrolling " + + "back into view", isSelected(getRow(0))); + } + + private void toggleFirstRowSelection() { + selectMenuPath("Component", "Body rows", "Select first row"); + } + + @SuppressWarnings("static-method") + private boolean isSelected(TestBenchElement row) { + /* + * FIXME We probably should get a GridRow instead of a plain + * TestBenchElement, that has an "isSelected" thing integrated. (henrik + * paul 26.6.2014) + */ + return row.getAttribute("class").contains("-row-selected"); + } + + private TestBenchElement getRow(int i) { + return getGridElement().getRow(i); + } + private void assertPrimaryStylename(String stylename) { assertTrue(getGridElement().getAttribute("class").contains(stylename)); diff --git a/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java index 91a4e19886..8ea652cc74 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue; import org.junit.Test; import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.DesiredCapabilities; import com.vaadin.testbench.By; import com.vaadin.testbench.TestBenchElement; @@ -43,6 +44,14 @@ public class GridClientRenderers extends MultiBrowserTest { private int latency = 0; @Override + protected DesiredCapabilities getDesiredCapabilities() { + DesiredCapabilities c = new DesiredCapabilities( + super.getDesiredCapabilities()); + c.setCapability("handlesAlerts", true); + return c; + } + + @Override protected Class<?> getUIClass() { return GridClientColumnRenderers.class; } |