Change-Id: I5ceb52dea079f48b0065c1b2dbdc35b30fe8c4eetags/7.2.0.beta1
@@ -22,7 +22,7 @@ import java.util.List; | |||
import com.google.gwt.core.client.Scheduler; | |||
import com.google.gwt.core.client.Scheduler.ScheduledCommand; | |||
import com.vaadin.client.Profiler; | |||
import com.vaadin.client.ui.grid.Range; | |||
import com.vaadin.shared.ui.grid.Range; | |||
/** | |||
* Base implementation for data sources that fetch data from a remote system. | |||
@@ -238,4 +238,86 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { | |||
Profiler.leave("AbstractRemoteDataSource.setRowData"); | |||
} | |||
/** | |||
* Informs this data source that the server has removed data. | |||
* | |||
* @param firstRowIndex | |||
* the index of the first removed row | |||
* @param count | |||
* the number of removed rows, starting from | |||
* <code>firstRowIndex</code> | |||
*/ | |||
protected void removeRowData(int firstRowIndex, int count) { | |||
Profiler.enter("AbstractRemoteDataSource.removeRowData"); | |||
// pack the cached data | |||
for (int i = 0; i < count; i++) { | |||
Integer oldIndex = Integer.valueOf(firstRowIndex + count + i); | |||
if (rowCache.containsKey(oldIndex)) { | |||
Integer newIndex = Integer.valueOf(firstRowIndex + i); | |||
rowCache.put(newIndex, rowCache.remove(oldIndex)); | |||
} | |||
} | |||
Range removedRange = Range.withLength(firstRowIndex, count); | |||
if (removedRange.intersects(cached)) { | |||
Range[] partitions = cached.partitionWith(removedRange); | |||
Range remainsBefore = partitions[0]; | |||
Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange | |||
.length()); | |||
cached = remainsBefore.combineWith(transposedRemainsAfter); | |||
} | |||
estimatedSize -= count; | |||
dataChangeHandler.dataRemoved(firstRowIndex, count); | |||
checkCacheCoverage(); | |||
Profiler.leave("AbstractRemoteDataSource.removeRowData"); | |||
} | |||
/** | |||
* Informs this data source that new data has been inserted from the server. | |||
* | |||
* @param firstRowIndex | |||
* the destination index of the new row data | |||
* @param count | |||
* the number of rows inserted | |||
*/ | |||
protected void insertRowData(int firstRowIndex, int count) { | |||
Profiler.enter("AbstractRemoteDataSource.insertRowData"); | |||
if (cached.contains(firstRowIndex)) { | |||
int oldCacheEnd = cached.getEnd(); | |||
/* | |||
* We need to invalidate the cache from the inserted row onwards, | |||
* since the cache wants to be a contiguous range. It doesn't | |||
* support holes. | |||
* | |||
* If holes were supported, we could shift the higher part of | |||
* "cached" and leave a hole the size of "count" in the middle. | |||
*/ | |||
cached = cached.splitAt(firstRowIndex)[0]; | |||
for (int i = firstRowIndex; i < oldCacheEnd; i++) { | |||
rowCache.remove(Integer.valueOf(i)); | |||
} | |||
} | |||
else if (firstRowIndex < cached.getStart()) { | |||
Range oldCached = cached; | |||
cached = cached.offsetBy(count); | |||
for (int i = 0; i < rowCache.size(); i++) { | |||
Integer oldIndex = Integer.valueOf(oldCached.getEnd() - i); | |||
Integer newIndex = Integer.valueOf(cached.getEnd() - i); | |||
rowCache.put(newIndex, rowCache.remove(oldIndex)); | |||
} | |||
} | |||
estimatedSize += count; | |||
dataChangeHandler.dataAdded(firstRowIndex, count); | |||
checkCacheCoverage(); | |||
Profiler.leave("AbstractRemoteDataSource.insertRowData"); | |||
} | |||
} |
@@ -56,6 +56,16 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { | |||
public void setRowData(int firstRow, List<String[]> rows) { | |||
dataSource.setRowData(firstRow, rows); | |||
} | |||
@Override | |||
public void removeRowData(int firstRow, int count) { | |||
dataSource.removeRowData(firstRow, count); | |||
} | |||
@Override | |||
public void insertRowData(int firstRow, int count) { | |||
dataSource.insertRowData(firstRow, count); | |||
} | |||
}); | |||
} | |||
@@ -35,12 +35,12 @@ 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.event.shared.HandlerRegistration; | |||
import com.google.gwt.user.client.DOM; | |||
import com.google.gwt.user.client.Element; | |||
import com.google.gwt.user.client.Window; | |||
import com.google.gwt.user.client.ui.UIObject; | |||
import com.google.gwt.user.client.ui.Widget; | |||
import com.google.web.bindery.event.shared.HandlerRegistration; | |||
import com.vaadin.client.Profiler; | |||
import com.vaadin.client.Util; | |||
import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle; | |||
@@ -50,6 +50,7 @@ import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition; | |||
import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition; | |||
import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle; | |||
import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle; | |||
import com.vaadin.shared.ui.grid.Range; | |||
import com.vaadin.shared.util.SharedUtil; | |||
/*- | |||
@@ -1633,6 +1634,8 @@ public class Escalator extends Widget { | |||
return; | |||
} | |||
boolean rowsWereMoved = false; | |||
final int topRowPos = getRowTop(visualRowOrder.getFirst()); | |||
// TODO [[mpixscroll]] | |||
final int scrollTop = tBodyScrollTop; | |||
@@ -1655,6 +1658,8 @@ public class Escalator extends Widget { | |||
final int logicalRowIndex = scrollTop / ROW_HEIGHT_PX; | |||
moveAndUpdateEscalatorRows(Range.between(start, end), 0, | |||
logicalRowIndex); | |||
rowsWereMoved = (rowsToMove != 0); | |||
} | |||
else if (viewportOffset + ROW_HEIGHT_PX <= 0) { | |||
@@ -1723,9 +1728,13 @@ public class Escalator extends Widget { | |||
.get(1)) - 1; | |||
moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex); | |||
} | |||
rowsWereMoved = (rowsToMove != 0); | |||
} | |||
fireRowVisibilityChangeEvent(); | |||
if (rowsWereMoved) { | |||
fireRowVisibilityChangeEvent(); | |||
} | |||
} | |||
@Override | |||
@@ -1805,6 +1814,8 @@ public class Escalator extends Widget { | |||
setRowPosition(tr, 0, rowTop); | |||
rowTop += ROW_HEIGHT_PX; | |||
} | |||
fireRowVisibilityChangeEvent(); | |||
} | |||
return addedRows; | |||
} | |||
@@ -1919,8 +1930,6 @@ public class Escalator extends Widget { | |||
newRowTop += ROW_HEIGHT_PX; | |||
} | |||
} | |||
fireRowVisibilityChangeEvent(); | |||
} | |||
/** | |||
@@ -3181,9 +3190,15 @@ public class Escalator extends Widget { | |||
*/ | |||
@Override | |||
public void setHeight(final String height) { | |||
final int escalatorRowsBefore = body.visualRowOrder.size(); | |||
super.setHeight(height != null && !height.isEmpty() ? height | |||
: DEFAULT_HEIGHT); | |||
recalculateElementSizes(); | |||
if (escalatorRowsBefore != body.visualRowOrder.size()) { | |||
fireRowVisibilityChangeEvent(); | |||
} | |||
} | |||
/** | |||
@@ -3437,26 +3452,30 @@ public class Escalator extends Widget { | |||
* Adds an event handler that gets notified when the range of visible rows | |||
* changes e.g. because of scrolling. | |||
* | |||
* @param rowVisibilityChangeHadler | |||
* @param rowVisibilityChangeHandler | |||
* the event handler | |||
* @return a handler registration for the added handler | |||
*/ | |||
public HandlerRegistration addRowVisibilityChangeHandler( | |||
RowVisibilityChangeHandler rowVisibilityChangeHadler) { | |||
return addHandler(rowVisibilityChangeHadler, | |||
RowVisibilityChangeHandler rowVisibilityChangeHandler) { | |||
return addHandler(rowVisibilityChangeHandler, | |||
RowVisibilityChangeEvent.TYPE); | |||
} | |||
private void fireRowVisibilityChangeEvent() { | |||
int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder | |||
.getFirst()); | |||
int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder | |||
.getLast()) + 1; | |||
if (!body.visualRowOrder.isEmpty()) { | |||
int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder | |||
.getFirst()); | |||
int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder | |||
.getLast()) + 1; | |||
int visibleRowCount = visibleRangeEnd - visibleRangeStart; | |||
int visibleRowCount = visibleRangeEnd - visibleRangeStart; | |||
fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, | |||
visibleRowCount)); | |||
fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, | |||
visibleRowCount)); | |||
} else { | |||
fireEvent(new RowVisibilityChangeEvent(0, 0)); | |||
} | |||
} | |||
/** |
@@ -21,6 +21,7 @@ import java.util.List; | |||
import com.google.gwt.core.shared.GWT; | |||
import com.google.gwt.dom.client.Element; | |||
import com.google.gwt.event.shared.HandlerRegistration; | |||
import com.google.gwt.user.client.ui.Composite; | |||
import com.google.gwt.user.client.ui.HasVisibility; | |||
import com.google.gwt.user.client.ui.Widget; | |||
@@ -73,6 +74,10 @@ public class Grid<T> extends Composite { | |||
*/ | |||
private final List<GridColumn<?, T>> columns = new ArrayList<GridColumn<?, T>>(); | |||
/** | |||
* The datasource currently in use. <em>Note:</em> it is <code>null</code> | |||
* on initialization, but not after that. | |||
*/ | |||
private DataSource<T> dataSource; | |||
/** | |||
@@ -1211,4 +1216,14 @@ public class Grid<T> extends Composite { | |||
public GridColumn<?, T> getLastFrozenColumn() { | |||
return lastFrozenColumn; | |||
} | |||
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); | |||
} | |||
} |
@@ -30,6 +30,7 @@ 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.GridColumnState; | |||
import com.vaadin.shared.ui.grid.GridServerRpc; | |||
import com.vaadin.shared.ui.grid.GridState; | |||
/** | |||
@@ -82,6 +83,21 @@ public class GridConnector extends AbstractComponentConnector { | |||
return (GridState) super.getState(); | |||
} | |||
@Override | |||
protected void init() { | |||
super.init(); | |||
getWidget().addRowVisibilityChangeHandler( | |||
new RowVisibilityChangeHandler() { | |||
@Override | |||
public void onRowVisibilityChange( | |||
RowVisibilityChangeEvent event) { | |||
getRpcProxy(GridServerRpc.class).setVisibleRows( | |||
event.getFirstVisibleRow(), | |||
event.getVisibleRowCount()); | |||
} | |||
}); | |||
} | |||
@Override | |||
public void onStateChanged(StateChangeEvent stateChangeEvent) { | |||
super.onStateChanged(stateChangeEvent); |
@@ -21,6 +21,8 @@ import static org.junit.Assert.assertTrue; | |||
import org.junit.Test; | |||
import com.vaadin.shared.ui.grid.Range; | |||
@SuppressWarnings("static-method") | |||
public class PartitioningTest { | |||
@@ -18,6 +18,7 @@ package com.vaadin.data; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import com.vaadin.data.Container.Indexed; | |||
@@ -67,22 +68,24 @@ public class RpcDataProviderExtension extends AbstractExtension { | |||
Collection<?> propertyIds = container.getContainerPropertyIds(); | |||
List<String[]> rows = new ArrayList<String[]>(itemIds.size()); | |||
for (Object itemId : itemIds) { | |||
Item item = container.getItem(itemId); | |||
String[] row = new String[propertyIds.size()]; | |||
int i = 0; | |||
for (Object propertyId : propertyIds) { | |||
Object value = item.getItemProperty(propertyId).getValue(); | |||
String stringValue = String.valueOf(value); | |||
row[i++] = stringValue; | |||
} | |||
rows.add(row); | |||
rows.add(getRowData(propertyIds, itemId)); | |||
} | |||
getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows); | |||
} | |||
private String[] getRowData(Collection<?> propertyIds, Object itemId) { | |||
Item item = container.getItem(itemId); | |||
String[] row = new String[propertyIds.size()]; | |||
int i = 0; | |||
for (Object propertyId : propertyIds) { | |||
Object value = item.getItemProperty(propertyId).getValue(); | |||
String stringValue = String.valueOf(value); | |||
row[i++] = stringValue; | |||
} | |||
return row; | |||
} | |||
@Override | |||
protected DataProviderState getState() { | |||
return (DataProviderState) super.getState(); | |||
@@ -98,4 +101,48 @@ public class RpcDataProviderExtension extends AbstractExtension { | |||
super.extend(component); | |||
} | |||
/** | |||
* Informs the client side that new rows have been inserted into the data | |||
* source. | |||
* | |||
* @param index | |||
* the index at which new rows have been inserted | |||
* @param count | |||
* the number of rows inserted at <code>index</code> | |||
*/ | |||
public void insertRowData(int index, int count) { | |||
getState().containerSize += count; | |||
getRpcProxy(DataProviderRpc.class).insertRowData(index, count); | |||
} | |||
/** | |||
* Informs the client side that rows have been removed from the data source. | |||
* | |||
* @param firstIndex | |||
* the index of the first row removed | |||
* @param count | |||
* the number of rows removed | |||
*/ | |||
public void removeRowData(int firstIndex, int count) { | |||
getState().containerSize -= count; | |||
getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count); | |||
} | |||
/** | |||
* Informs the client side that data of a row has been modified in the data | |||
* source. | |||
* | |||
* @param index | |||
* the index of the row that was updated | |||
*/ | |||
public void updateRowData(int index) { | |||
/* | |||
* TODO: ignore duplicate requests for the same index during the same | |||
* roundtrip. | |||
*/ | |||
Object itemId = container.getIdByIndex(index); | |||
String[] row = getRowData(container.getContainerPropertyIds(), itemId); | |||
getRpcProxy(DataProviderRpc.class).setRowData(index, | |||
Collections.singletonList(row)); | |||
} | |||
} |
@@ -26,15 +26,28 @@ import java.util.List; | |||
import java.util.Map; | |||
import com.vaadin.data.Container; | |||
import com.vaadin.data.Container.Indexed.ItemAddEvent; | |||
import com.vaadin.data.Container.Indexed.ItemRemoveEvent; | |||
import com.vaadin.data.Container.ItemSetChangeEvent; | |||
import com.vaadin.data.Container.ItemSetChangeListener; | |||
import com.vaadin.data.Container.ItemSetChangeNotifier; | |||
import com.vaadin.data.Container.PropertySetChangeEvent; | |||
import com.vaadin.data.Container.PropertySetChangeListener; | |||
import com.vaadin.data.Container.PropertySetChangeNotifier; | |||
import com.vaadin.data.Item; | |||
import com.vaadin.data.Property; | |||
import com.vaadin.data.Property.ValueChangeEvent; | |||
import com.vaadin.data.Property.ValueChangeListener; | |||
import com.vaadin.data.Property.ValueChangeNotifier; | |||
import com.vaadin.data.RpcDataProviderExtension; | |||
import com.vaadin.server.KeyMapper; | |||
import com.vaadin.shared.ui.grid.ColumnGroupRowState; | |||
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.Range; | |||
import com.vaadin.ui.AbstractComponent; | |||
import com.vaadin.ui.Component; | |||
/** | |||
* Data grid component | |||
@@ -56,6 +69,282 @@ import com.vaadin.ui.AbstractComponent; | |||
*/ | |||
public class Grid extends AbstractComponent { | |||
/** | |||
* 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 | |||
* requried). | |||
* <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>attaching and detaching {@link Component Components} from the Vaadin | |||
* Component hierarchy. | |||
* </ul> | |||
*/ | |||
private final class ActiveRowHandler { | |||
/** | |||
* A map from itemId to the value change listener used for all of its | |||
* properties | |||
*/ | |||
private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>(); | |||
/** | |||
* The currently active range. Practically, it's the range of row | |||
* indices being displayed currently. | |||
*/ | |||
private Range activeRange = Range.withLength(0, 0); | |||
/** | |||
* A hook for making sure that appropriate data is "active". All other | |||
* rows should be "inactive". | |||
* <p> | |||
* "Active" can mean different things in different contexts. For | |||
* example, only the Properties in the active range need | |||
* ValueChangeListeners. Also, whenever a row with a Component becomes | |||
* active, it needs to be attached (and conversely, when inactive, it | |||
* needs to be detached). | |||
* | |||
* @param firstActiveRow | |||
* the first active row | |||
* @param activeRowCount | |||
* the number of active rows | |||
*/ | |||
public void setActiveRows(int firstActiveRow, int activeRowCount) { | |||
final Range newActiveRange = Range.withLength(firstActiveRow, | |||
activeRowCount); | |||
// TODO [[Components]] attach and detach components | |||
/*- | |||
* Example | |||
* | |||
* New Range: [3, 4, 5, 6, 7] | |||
* Old Range: [1, 2, 3, 4, 5] | |||
* Result: [1, 2][3, 4, 5] [] | |||
*/ | |||
final Range[] depractionPartition = activeRange | |||
.partitionWith(newActiveRange); | |||
removeValueChangeListeners(depractionPartition[0]); | |||
removeValueChangeListeners(depractionPartition[2]); | |||
/*- | |||
* Example | |||
* | |||
* Old Range: [1, 2, 3, 4, 5] | |||
* New Range: [3, 4, 5, 6, 7] | |||
* Result: [] [3, 4, 5][6, 7] | |||
*/ | |||
final Range[] activationPartition = newActiveRange | |||
.partitionWith(activeRange); | |||
addValueChangeListeners(activationPartition[0]); | |||
addValueChangeListeners(activationPartition[2]); | |||
activeRange = newActiveRange; | |||
} | |||
private void addValueChangeListeners(Range range) { | |||
for (int i = range.getStart(); i < range.getEnd(); i++) { | |||
final Object itemId = datasource.getIdByIndex(i); | |||
final Item item = datasource.getItem(itemId); | |||
if (valueChangeListeners.containsKey(itemId)) { | |||
/* | |||
* This might occur when items are removed from above the | |||
* viewport, the escalator scrolls up to compensate, but the | |||
* same items remain in the view: It looks as if one row was | |||
* scrolled, when in fact the whole viewport was shifted up. | |||
*/ | |||
continue; | |||
} | |||
GridValueChangeListener listener = new GridValueChangeListener( | |||
itemId); | |||
valueChangeListeners.put(itemId, listener); | |||
for (final Object propertyId : item.getItemPropertyIds()) { | |||
final Property<?> property = item | |||
.getItemProperty(propertyId); | |||
if (property instanceof ValueChangeNotifier) { | |||
((ValueChangeNotifier) property) | |||
.addValueChangeListener(listener); | |||
} | |||
} | |||
} | |||
} | |||
private void removeValueChangeListeners(Range range) { | |||
for (int i = range.getStart(); i < range.getEnd(); i++) { | |||
final Object itemId = datasource.getIdByIndex(i); | |||
final Item item = datasource.getItem(itemId); | |||
final GridValueChangeListener listener = valueChangeListeners | |||
.remove(itemId); | |||
if (listener != null) { | |||
for (final Object propertyId : item.getItemPropertyIds()) { | |||
final Property<?> property = item | |||
.getItemProperty(propertyId); | |||
/* | |||
* Because listener != null, we can be certain that this | |||
* property is a ValueChangeNotifier: It wouldn't be | |||
* inserted in addValueChangeListeners if the property | |||
* wasn't a suitable type. I.e. No need for "instanceof" | |||
* check. | |||
*/ | |||
((ValueChangeNotifier) property) | |||
.removeValueChangeListener(listener); | |||
} | |||
} | |||
} | |||
} | |||
public void clear() { | |||
removeValueChangeListeners(activeRange); | |||
/* | |||
* we're doing an assert for emptiness there (instead of a | |||
* carte-blanche ".clear()"), to be absolutely sure that everything | |||
* is cleaned up properly, and that we have no dangling listeners. | |||
*/ | |||
assert valueChangeListeners.isEmpty() : "GridValueChangeListeners are leaking"; | |||
activeRange = Range.withLength(0, 0); | |||
} | |||
/** | |||
* Manages removed properties in active rows. | |||
* | |||
* @param removedPropertyIds | |||
* the property ids that have been removed from the container | |||
*/ | |||
public void propertiesRemoved(Collection<Object> removedPropertyIds) { | |||
/* | |||
* no-op, for now. | |||
* | |||
* The Container should be responsible for cleaning out any | |||
* ValueChangeListeners from removed Properties. Components will | |||
* benefit from this, however. | |||
*/ | |||
} | |||
/** | |||
* Manages added properties in active rows. | |||
* | |||
* @param addedPropertyIds | |||
* the property ids that have been added to the container | |||
*/ | |||
public void propertiesAdded(Collection<Object> addedPropertyIds) { | |||
for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) { | |||
final Object itemId = datasource.getIdByIndex(i); | |||
final Item item = datasource.getItem(itemId); | |||
final GridValueChangeListener listener = valueChangeListeners | |||
.get(itemId); | |||
assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners"; | |||
for (final Object propertyId : addedPropertyIds) { | |||
final Property<?> property = item | |||
.getItemProperty(propertyId); | |||
if (property instanceof ValueChangeNotifier) { | |||
((ValueChangeNotifier) property) | |||
.addValueChangeListener(listener); | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Handles the insertion of rows. | |||
* <p> | |||
* This method's responsibilities are to: | |||
* <ul> | |||
* <li>shift the internal bookkeeping by <code>count</code> if the | |||
* insertion happens above currently active range | |||
* <li>ignore rows inserted below the currently active range | |||
* <li>shift (and deactivate) rows pushed out of view | |||
* <li>activate rows that are inserted in the current viewport | |||
* </ul> | |||
* | |||
* @param firstIndex | |||
* the index of the first inserted rows | |||
* @param count | |||
* the number of rows inserted at <code>firstIndex</code> | |||
*/ | |||
public void insertRows(int firstIndex, int count) { | |||
if (firstIndex < activeRange.getStart()) { | |||
activeRange = activeRange.offsetBy(count); | |||
} else if (firstIndex < activeRange.getEnd()) { | |||
final Range deprecatedRange = Range.withLength( | |||
activeRange.getEnd(), count); | |||
removeValueChangeListeners(deprecatedRange); | |||
final Range freshRange = Range.between(firstIndex, count); | |||
addValueChangeListeners(freshRange); | |||
} else { | |||
// out of view, noop | |||
} | |||
} | |||
/** | |||
* Removes a single item by its id. | |||
* | |||
* @param itemId | |||
* the id of the removed id. <em>Note:</em> this item does | |||
* not exist anymore in the datasource | |||
*/ | |||
public void removeItemId(Object itemId) { | |||
final GridValueChangeListener removedListener = valueChangeListeners | |||
.remove(itemId); | |||
if (removedListener != null) { | |||
/* | |||
* We removed an item from somewhere in the visible range, so we | |||
* make the active range shorter. The empty hole will be filled | |||
* by the client-side code when it asks for more information. | |||
*/ | |||
activeRange = Range.withLength(activeRange.getStart(), | |||
activeRange.length() - 1); | |||
} | |||
} | |||
} | |||
/** | |||
* A class to listen to changes in property values in the Container added | |||
* with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies | |||
* the data source to update the client-side representation of the modified | |||
* item. | |||
* <p> | |||
* One instance of this class can (and should) be reused for all the | |||
* properties in an item, since this class will inform that the entire row | |||
* needs to be re-evaluated (in contrast to a property-based change | |||
* management) | |||
* <p> | |||
* Since there's no Container-wide possibility to listen to any kind of | |||
* value changes, an instance of this class needs to be attached to each and | |||
* every Item's Property in the container. | |||
* | |||
* @see Grid#addValueChangeListener(Container, Object, Object) | |||
* @see Grid#valueChangeListeners | |||
*/ | |||
private class GridValueChangeListener implements ValueChangeListener { | |||
private final Object itemId; | |||
public GridValueChangeListener(Object itemId) { | |||
/* | |||
* Using an assert instead of an exception throw, just to optimize | |||
* prematurely | |||
*/ | |||
assert itemId != null : "null itemId not accepted"; | |||
this.itemId = itemId; | |||
} | |||
@Override | |||
public void valueChange(ValueChangeEvent event) { | |||
datasourceExtension.updateRowData(datasource.indexOfId(itemId)); | |||
} | |||
} | |||
/** | |||
* The data source attached to the grid | |||
*/ | |||
@@ -98,13 +387,17 @@ public class Grid extends AbstractComponent { | |||
columnKeys.remove(columnId); | |||
getState().columns.remove(column.getState()); | |||
} | |||
activeRowHandler.propertiesRemoved(removedColumns); | |||
// Add new columns | |||
HashSet<Object> addedPropertyIds = new HashSet<Object>(); | |||
for (Object propertyId : properties) { | |||
if (!columns.containsKey(propertyId)) { | |||
appendColumn(propertyId); | |||
addedPropertyIds.add(propertyId); | |||
} | |||
} | |||
activeRowHandler.propertiesAdded(addedPropertyIds); | |||
Object frozenPropertyId = columnKeys | |||
.get(getState(false).lastFrozenColumnId); | |||
@@ -114,8 +407,53 @@ public class Grid extends AbstractComponent { | |||
} | |||
}; | |||
private ItemSetChangeListener itemListener = new ItemSetChangeListener() { | |||
@Override | |||
public void containerItemSetChange(ItemSetChangeEvent event) { | |||
if (event instanceof ItemAddEvent) { | |||
ItemAddEvent addEvent = (ItemAddEvent) event; | |||
int firstIndex = addEvent.getFirstIndex(); | |||
int count = addEvent.getAddedItemsCount(); | |||
datasourceExtension.insertRowData(firstIndex, count); | |||
activeRowHandler.insertRows(firstIndex, count); | |||
} | |||
else if (event instanceof ItemRemoveEvent) { | |||
ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; | |||
int firstIndex = removeEvent.getFirstIndex(); | |||
int count = removeEvent.getRemovedItemsCount(); | |||
datasourceExtension.removeRowData(firstIndex, count); | |||
/* | |||
* Unfortunately, there's no sane way of getting the rest of the | |||
* removed itemIds. | |||
* | |||
* 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(removeEvent.getFirstItemId()); | |||
} | |||
else { | |||
// TODO no diff info available, redraw everything | |||
throw new UnsupportedOperationException("bare " | |||
+ "ItemSetChangeEvents are currently " | |||
+ "not supported, use a container that " | |||
+ "uses AddItemEvents and RemoveItemEvents."); | |||
} | |||
} | |||
}; | |||
private RpcDataProviderExtension datasourceExtension; | |||
private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); | |||
/** | |||
* Creates a new Grid using the given datasource. | |||
* | |||
@@ -124,6 +462,14 @@ public class Grid extends AbstractComponent { | |||
*/ | |||
public Grid(Container.Indexed datasource) { | |||
setContainerDatasource(datasource); | |||
registerRpc(new GridServerRpc() { | |||
@Override | |||
public void setVisibleRows(int firstVisibleRow, int visibleRowCount) { | |||
activeRowHandler | |||
.setActiveRows(firstVisibleRow, visibleRowCount); | |||
} | |||
}); | |||
} | |||
/** | |||
@@ -143,11 +489,16 @@ public class Grid extends AbstractComponent { | |||
return; | |||
} | |||
// Remove old listener | |||
// Remove old listeners | |||
if (datasource instanceof PropertySetChangeNotifier) { | |||
((PropertySetChangeNotifier) datasource) | |||
.removePropertySetChangeListener(propertyListener); | |||
} | |||
if (datasource instanceof ItemSetChangeNotifier) { | |||
((ItemSetChangeNotifier) datasource) | |||
.removeItemSetChangeListener(itemListener); | |||
} | |||
activeRowHandler.clear(); | |||
if (datasourceExtension != null) { | |||
removeExtension(datasourceExtension); | |||
@@ -162,6 +513,15 @@ public class Grid extends AbstractComponent { | |||
((PropertySetChangeNotifier) datasource) | |||
.addPropertySetChangeListener(propertyListener); | |||
} | |||
if (datasource instanceof ItemSetChangeNotifier) { | |||
((ItemSetChangeNotifier) datasource) | |||
.addItemSetChangeListener(itemListener); | |||
} | |||
/* | |||
* activeRowHandler will be updated by the client-side request that | |||
* occurs on container change - no need to actively re-insert any | |||
* ValueChangeListeners at this point. | |||
*/ | |||
getState().columns.clear(); | |||
setLastFrozenPropertyId(null); |
@@ -37,4 +37,25 @@ public interface DataProviderRpc extends ClientRpc { | |||
* the updated row data | |||
*/ | |||
public void setRowData(int firstRowIndex, List<String[]> rowData); | |||
/** | |||
* Informs the client to remove row data. | |||
* | |||
* @param firstRowIndex | |||
* the index of the first removed row | |||
* @param count | |||
* the number of rows removed from <code>firstRowIndex</code> and | |||
* onwards | |||
*/ | |||
public void removeRowData(int firstRowIndex, int count); | |||
/** | |||
* Informs the client to insert new row data. | |||
* | |||
* @param firstRowIndex | |||
* the index of the first new row | |||
* @param count | |||
* the number of rows inserted at <code>firstRowIndex</code> | |||
*/ | |||
public void insertRowData(int firstRowIndex, int count); | |||
} |
@@ -0,0 +1,39 @@ | |||
/* | |||
* Copyright 2000-2013 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 com.vaadin.shared.communication.ServerRpc; | |||
/** | |||
* TODO | |||
* | |||
* @since 7.2 | |||
* @author Vaadin Ltd | |||
*/ | |||
public interface GridServerRpc extends ServerRpc { | |||
/** | |||
* TODO | |||
* | |||
* @param firstVisibleRow | |||
* the index of the first visible row | |||
* @param visibleRowCount | |||
* the number of rows visible, counted from | |||
* <code>firstVisibleRow</code> | |||
*/ | |||
void setVisibleRows(int firstVisibleRow, int visibleRowCount); | |||
} |
@@ -14,7 +14,7 @@ | |||
* the License. | |||
*/ | |||
package com.vaadin.client.ui.grid; | |||
package com.vaadin.shared.ui.grid; | |||
/** | |||
* An immutable representation of a range, marked by start and end points. |
@@ -13,7 +13,7 @@ | |||
* License for the specific language governing permissions and limitations under | |||
* the License. | |||
*/ | |||
package com.vaadin.client.ui.grid; | |||
package com.vaadin.shared.ui.grid; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertTrue; |
@@ -40,20 +40,22 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
private final int ROWS = 1000; | |||
private IndexedContainer ds; | |||
@Override | |||
protected Grid constructComponent() { | |||
// Build data source | |||
IndexedContainer ds = new IndexedContainer(); | |||
ds = new IndexedContainer(); | |||
for (int col = 0; col < COLUMNS; col++) { | |||
ds.addContainerProperty("Column" + col, String.class, ""); | |||
ds.addContainerProperty(getColumnProperty(col), String.class, ""); | |||
} | |||
for (int row = 0; row < ROWS; row++) { | |||
Item item = ds.addItem(Integer.valueOf(row)); | |||
for (int col = 0; col < COLUMNS; col++) { | |||
item.getItemProperty("Column" + col).setValue( | |||
item.getItemProperty(getColumnProperty(col)).setValue( | |||
"(" + row + ", " + col + ")"); | |||
} | |||
} | |||
@@ -63,7 +65,8 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
// Add footer values (header values are automatically created) | |||
for (int col = 0; col < COLUMNS; col++) { | |||
grid.getColumn("Column" + col).setFooterCaption("Footer " + col); | |||
grid.getColumn(getColumnProperty(col)).setFooterCaption( | |||
"Footer " + col); | |||
} | |||
// Set varying column widths | |||
@@ -81,6 +84,8 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
createColumnGroupActions(); | |||
createRowActions(); | |||
return grid; | |||
} | |||
@@ -131,9 +136,9 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
createCategory("Columns", null); | |||
for (int c = 0; c < COLUMNS; c++) { | |||
createCategory("Column" + c, "Columns"); | |||
createCategory(getColumnProperty(c), "Columns"); | |||
createBooleanAction("Visible", "Column" + c, true, | |||
createBooleanAction("Visible", getColumnProperty(c), true, | |||
new Command<Grid, Boolean>() { | |||
@Override | |||
@@ -148,7 +153,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
} | |||
}, c); | |||
createClickAction("Remove", "Column" + c, | |||
createClickAction("Remove", getColumnProperty(c), | |||
new Command<Grid, String>() { | |||
@Override | |||
@@ -158,7 +163,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
} | |||
}, null, c); | |||
createClickAction("Freeze", "Column" + c, | |||
createClickAction("Freeze", getColumnProperty(c), | |||
new Command<Grid, String>() { | |||
@Override | |||
@@ -167,7 +172,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
} | |||
}, null, c); | |||
createCategory("Column" + c + " Width", "Column" + c); | |||
createCategory("Column" + c + " Width", getColumnProperty(c)); | |||
createClickAction("Auto", "Column" + c + " Width", | |||
new Command<Grid, Integer>() { | |||
@@ -203,6 +208,10 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
} | |||
} | |||
private static String getColumnProperty(int c) { | |||
return "Column" + c; | |||
} | |||
protected void createColumnGroupActions() { | |||
createCategory("Column groups", null); | |||
@@ -269,6 +278,58 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { | |||
} | |||
protected void createRowActions() { | |||
createCategory("Body rows", null); | |||
createClickAction("Add first row", "Body rows", | |||
new Command<Grid, String>() { | |||
@Override | |||
public void execute(Grid c, String value, Object data) { | |||
Item item = ds.addItemAt(0, new Object()); | |||
for (int i = 0; i < COLUMNS; i++) { | |||
item.getItemProperty(getColumnProperty(i)) | |||
.setValue("newcell: " + i); | |||
} | |||
} | |||
}, null); | |||
createClickAction("Remove first row", "Body rows", | |||
new Command<Grid, String>() { | |||
@Override | |||
public void execute(Grid c, String value, Object data) { | |||
Object firstItemId = ds.getIdByIndex(0); | |||
ds.removeItem(firstItemId); | |||
} | |||
}, null); | |||
createClickAction("Modify first row (getItemProperty)", "Body rows", | |||
new Command<Grid, String>() { | |||
@Override | |||
public void execute(Grid c, String value, Object data) { | |||
Object firstItemId = ds.getIdByIndex(0); | |||
Item item = ds.getItem(firstItemId); | |||
for (int i = 0; i < COLUMNS; i++) { | |||
item.getItemProperty(getColumnProperty(i)) | |||
.setValue("modified: " + i); | |||
} | |||
} | |||
}, null); | |||
createClickAction("Modify first row (getContainerProperty)", | |||
"Body rows", new Command<Grid, String>() { | |||
@Override | |||
public void execute(Grid c, String value, Object data) { | |||
Object firstItemId = ds.getIdByIndex(0); | |||
for (Object containerPropertyId : ds | |||
.getContainerPropertyIds()) { | |||
ds.getContainerProperty(firstItemId, | |||
containerPropertyId).setValue( | |||
"modified: " + containerPropertyId); | |||
} | |||
} | |||
}, null); | |||
} | |||
@Override | |||
protected Integer getTicketNumber() { | |||
return 12829; |
@@ -22,6 +22,7 @@ import java.util.List; | |||
import org.junit.Test; | |||
import org.openqa.selenium.By; | |||
import org.openqa.selenium.NoSuchElementException; | |||
import org.openqa.selenium.WebElement; | |||
import org.openqa.selenium.interactions.Actions; | |||
@@ -253,6 +254,55 @@ public class GridBasicFeaturesTest extends MultiBrowserTest { | |||
assertPrimaryStylename("v-grid"); | |||
} | |||
/** | |||
* Test that the current view is updated when a server-side container change | |||
* occurs (without scrolling back and forth) | |||
*/ | |||
@Test | |||
public void testItemSetChangeEvent() throws Exception { | |||
openTestURL(); | |||
final By newRow = By.xpath("//td[text()='newcell: 0']"); | |||
assertTrue("Unexpected initial state", !elementIsFound(newRow)); | |||
selectMenuPath("Component", "Body rows", "Add first row"); | |||
assertTrue("Add row failed", elementIsFound(newRow)); | |||
selectMenuPath("Component", "Body rows", "Remove first row"); | |||
assertTrue("Remove row failed", !elementIsFound(newRow)); | |||
} | |||
/** | |||
* Test that the current view is updated when a property's value is reflect | |||
* to the client, when the value is modified server-side. | |||
*/ | |||
@Test | |||
public void testPropertyValueChangeEvent() throws Exception { | |||
openTestURL(); | |||
assertEquals("Unexpected cell initial state", "(0, 0)", | |||
getBodyCellByRowAndColumn(1, 1).getText()); | |||
selectMenuPath("Component", "Body rows", | |||
"Modify first row (getItemProperty)"); | |||
assertEquals("(First) modification with getItemProperty failed", | |||
"modified: 0", getBodyCellByRowAndColumn(1, 1).getText()); | |||
selectMenuPath("Component", "Body rows", | |||
"Modify first row (getContainerProperty)"); | |||
assertEquals("(Second) modification with getItemProperty failed", | |||
"modified: Column0", getBodyCellByRowAndColumn(1, 1).getText()); | |||
} | |||
private boolean elementIsFound(By locator) { | |||
try { | |||
return driver.findElement(locator) != null; | |||
} catch (NoSuchElementException e) { | |||
return false; | |||
} | |||
} | |||
private void assertPrimaryStylename(String stylename) { | |||
assertTrue(getGridElement().getAttribute("class").contains(stylename)); | |||