diff options
Diffstat (limited to 'server/src/com')
28 files changed, 5053 insertions, 18 deletions
diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java index 8e99bac541..c58d37d5fe 100644 --- a/server/src/com/vaadin/data/Container.java +++ b/server/src/com/vaadin/data/Container.java @@ -582,6 +582,60 @@ public interface Container extends Serializable { public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException; + /** + * An <code>Event</code> object specifying information about the added + * items. + */ + public interface ItemAddEvent extends ItemSetChangeEvent { + + /** + * Gets the item id of the first added item. + * + * @return item id of the first added item + */ + public Object getFirstItemId(); + + /** + * Gets the index of the first added item. + * + * @return index of the first added item + */ + public int getFirstIndex(); + + /** + * Gets the number of the added items. + * + * @return the number of added items. + */ + public int getAddedItemsCount(); + } + + /** + * An <code>Event</code> object specifying information about the removed + * items. + */ + public interface ItemRemoveEvent extends ItemSetChangeEvent { + /** + * Gets the item id of the first removed item. + * + * @return item id of the first removed item + */ + public Object getFirstItemId(); + + /** + * Gets the index of the first removed item. + * + * @return index of the first removed item + */ + public int getFirstIndex(); + + /** + * Gets the number of the removed items. + * + * @return the number of removed items + */ + public int getRemovedItemsCount(); + } } /** diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java new file mode 100644 index 0000000000..f731e4575d --- /dev/null +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -0,0 +1,873 @@ +/* + * 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.data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.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; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.server.AbstractExtension; +import com.vaadin.server.ClientConnector; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataProviderState; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; +import com.vaadin.ui.components.grid.Renderer; + +/** + * Provides Vaadin server-side container data source to a + * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently + * implemented as an Extension hardcoded to support a specific connector type. + * This will be changed once framework support for something more flexible has + * been implemented. + * + * @since + * @author Vaadin Ltd + */ +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 implements Serializable { + 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; + } + + /** + * Gets the registered item id based on its key. + * <p> + * A key is used to identify a particular row on both a server and a + * client. This method can be used to get the item id for the row key + * that the client has sent. + * + * @param key + * the row key for which to retrieve an item id + * @return the item id corresponding to {@code key} + * @throws IllegalStateException + * if the key mapper does not have a record of {@code key} . + */ + public 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 index = indexToItemId.inverse().get(itemId); + if (index == null || !activeRange.contains(index.intValue())) { + itemIdToKey.remove(itemId); + indexToItemId.remove(index); + } + } + + /** + * 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 + * required). + * <p> + * This bookeeping includes, but is not limited to: + * <ul> + * <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> + */ + private class ActiveRowHandler implements Serializable { + /** + * 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 cached 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 = container.getIdByIndex(i); + final Item item = container.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 = container.getIdByIndex(i); + final Item item = container.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .remove(itemId); + + if (listener != null) { + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .removeValueChangeListener(listener); + } + } + } + } + } + + /** + * Manages removed properties in active rows. + * + * @param removedPropertyIds + * the property ids that have been removed from the container + */ + public void propertiesRemoved(@SuppressWarnings("unused") + 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 = container.getIdByIndex(i); + final Item item = container.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) { + updateRowData(container.indexOfId(itemId)); + } + } + + private final Indexed container; + + private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); + + private final 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(); + insertRowData(firstIndex, count); + } + + else if (event instanceof ItemRemoveEvent) { + ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + int firstIndex = removeEvent.getFirstIndex(); + int count = removeEvent.getRemovedItemsCount(); + removeRowData(firstIndex, count); + } + + else { + Range visibleRows = activeRowHandler.activeRange; + List<?> itemIds = container.getItemIds(visibleRows.getStart(), + visibleRows.length()); + + keyMapper.removeActiveRows(keyMapper.activeRange); + keyMapper.addActiveRows(visibleRows, visibleRows.getStart(), + itemIds); + + pushRows(visibleRows.getStart(), itemIds); + activeRowHandler.setActiveRows(visibleRows.getStart(), + visibleRows.length()); + } + } + }; + + private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper(); + + /** + * Creates a new data provider using the given container. + * + * @param container + * the container to make available + */ + public RpcDataProviderExtension(Indexed container) { + this.container = container; + + registerRpc(new DataRequestRpc() { + @Override + public void requestRows(int firstRow, int numberOfRows, + int firstCachedRowIndex, int cacheSize) { + Range active = Range.withLength(firstRow, numberOfRows); + if (cacheSize != 0) { + Range cached = Range.withLength(firstCachedRowIndex, + cacheSize); + 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()); + } + }); + + getState().containerSize = container.size(); + + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .addItemSetChangeListener(itemListener); + } + + } + + private void pushRows(int firstRow, List<?> itemIds) { + Collection<?> propertyIds = container.getContainerPropertyIds(); + JSONArray rows = new JSONArray(); + for (Object itemId : itemIds) { + rows.put(getRowData(propertyIds, itemId)); + } + String jsonString = rows.toString(); + getRpcProxy(DataProviderRpc.class).setRowData(firstRow, jsonString); + } + + private JSONObject getRowData(Collection<?> propertyIds, Object itemId) { + Item item = container.getItem(itemId); + String[] row = new String[propertyIds.size()]; + + JSONArray rowData = new JSONArray(); + + Grid grid = getGrid(); + try { + for (Object propertyId : propertyIds) { + GridColumn column = grid.getColumn(propertyId); + + Object propertyValue = item.getItemProperty(propertyId) + .getValue(); + Object encodedValue = encodeValue(propertyValue, + column.getRenderer(), column.getConverter(), + grid.getLocale()); + + rowData.put(encodedValue); + } + + final JSONObject rowObject = new JSONObject(); + rowObject.put(GridState.JSONKEY_DATA, rowData); + rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId)); + return rowObject; + } catch (final JSONException e) { + throw new RuntimeException("Grid was unable to serialize " + + "data for row (this should've been caught " + + "eariler by other Grid logic)", e); + } + } + + @Override + protected DataProviderState getState() { + return (DataProviderState) super.getState(); + } + + /** + * Makes the data source available to the given {@link Grid} component. + * + * @param component + * the remote data grid component to extend + */ + public void extend(Grid component) { + 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> + */ + private void insertRowData(int index, int count) { + getState().containerSize += count; + getRpcProxy(DataProviderRpc.class).insertRowData(index, count); + + activeRowHandler.insertRows(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 + * @param firstItemId + * the item id of the first removed item + */ + private void removeRowData(int firstIndex, int count) { + getState().containerSize -= count; + getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count); + + for (int i = 0; i < count; i++) { + Object itemId = keyMapper.itemIdAtIndex(firstIndex + i); + if (itemId != null) { + activeRowHandler.removeItemId(itemId); + } + } + } + + /** + * 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); + JSONObject row = getRowData(container.getContainerPropertyIds(), itemId); + JSONArray rowArray = new JSONArray(Collections.singleton(row)); + String jsonString = rowArray.toString(); + getRpcProxy(DataProviderRpc.class).setRowData(index, jsonString); + } + + @Override + public void setParent(ClientConnector parent) { + super.setParent(parent); + if (parent == null) { + // We're detached, release various listeners + + activeRowHandler + .removeValueChangeListeners(activeRowHandler.activeRange); + + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .removeItemSetChangeListener(itemListener); + } + + } + } + + /** + * Informs this data provider that some of the properties have been removed + * from the container. + * <p> + * Please note that we could add our own + * {@link com.vaadin.data.Container.PropertySetChangeListener + * PropertySetChangeListener} to the container, but then we'd need to + * implement the same bookeeping for finding what's added and removed that + * Grid already does in its own listener. + * + * @param removedColumns + * a list of property ids for the removed columns + */ + public void propertiesRemoved(List<Object> removedColumns) { + activeRowHandler.propertiesRemoved(removedColumns); + } + + /** + * Informs this data provider that some of the properties have been added to + * the container. + * <p> + * Please note that we could add our own + * {@link com.vaadin.data.Container.PropertySetChangeListener + * PropertySetChangeListener} to the container, but then we'd need to + * implement the same bookeeping for finding what's added and removed that + * Grid already does in its own listener. + * + * @param addedPropertyIds + * a list of property ids for the added columns + */ + public void propertiesAdded(HashSet<Object> addedPropertyIds) { + activeRowHandler.propertiesAdded(addedPropertyIds); + } + + public DataProviderKeyMapper getKeyMapper() { + return keyMapper; + } + + protected Grid getGrid() { + return (Grid) getParent(); + } + + /** + * Converts and encodes the given data model property value using the given + * converter and renderer. This method is public only for testing purposes. + * + * @param renderer + * the renderer to use + * @param converter + * the converter to use + * @param modelValue + * the value to convert and encode + * @param locale + * the locale to use in conversion + * @return an encoded value ready to be sent to the client + */ + public static <T> Object encodeValue(Object modelValue, + Renderer<T> renderer, Converter<?, ?> converter, Locale locale) { + Class<T> presentationType = renderer.getPresentationType(); + T presentationValue; + + if (converter == null) { + try { + presentationValue = presentationType.cast(modelValue); + } catch (ClassCastException e) { + throw new Converter.ConversionException( + "Unable to convert value of type " + + modelValue.getClass().getName() + + " to presentation type " + + presentationType.getName() + + ". No converter is set and the types are not compatible."); + } + } else { + assert presentationType.isAssignableFrom(converter + .getPresentationType()); + @SuppressWarnings("unchecked") + Converter<T, Object> safeConverter = (Converter<T, Object>) converter; + presentationValue = safeConverter.convertToPresentation(modelValue, + safeConverter.getPresentationType(), locale); + } + + Object encodedValue = renderer.encode(presentationValue); + + /* + * because this is a relatively heavy operation, we'll hide this behind + * an assert so that the check will be removed in production mode + */ + assert jsonSupports(encodedValue) : "org.json.JSONObject does not know how to serialize objects of type " + + encodedValue.getClass().getName(); + return encodedValue; + } + + private static boolean jsonSupports(Object encodedValue) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.accumulate("test", encodedValue); + } catch (JSONException e) { + return false; + } + return true; + } +} diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java index adf6313770..0559585e14 100644 --- a/server/src/com/vaadin/data/util/AbstractBeanContainer.java +++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java @@ -222,6 +222,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends @Override public boolean removeAllItems() { int origSize = size(); + IDTYPE firstItem = getFirstVisibleItem(); internalRemoveAllItems(); @@ -234,7 +235,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends // fire event only if the visible view changed, regardless of whether // filtered out items were removed or not if (origSize != 0) { - fireItemSetChange(); + fireItemsRemoved(0, firstItem, origSize); } return true; @@ -679,6 +680,8 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends protected void addAll(Collection<? extends BEANTYPE> collection) throws IllegalStateException, IllegalArgumentException { boolean modified = false; + int origSize = size(); + for (BEANTYPE bean : collection) { // TODO skipping invalid beans - should not allow them in javadoc? if (bean == null @@ -699,13 +702,22 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends if (modified) { // Filter the contents when all items have been added if (isFiltered()) { - filterAll(); - } else { - fireItemSetChange(); + doFilterContainer(!getFilters().isEmpty()); + } + if (visibleNewItemsWasAdded(origSize)) { + // fire event about added items + int firstPosition = origSize; + IDTYPE firstItemId = getVisibleItemIds().get(firstPosition); + int affectedItems = size() - origSize; + fireItemsAdded(firstPosition, firstItemId, affectedItems); } } } + private boolean visibleNewItemsWasAdded(int origSize) { + return size() > origSize; + } + /** * Use the bean resolver to get the identifier for a bean. * diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java index b19cddb021..5ddc11ec6f 100644 --- a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java +++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java @@ -15,8 +15,10 @@ */ package com.vaadin.data.util; +import java.io.Serializable; import java.util.Collection; import java.util.Collections; +import java.util.EventObject; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -146,6 +148,85 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } } + private static abstract class BaseItemAddOrRemoveEvent extends + EventObject implements Serializable { + protected Object itemId; + protected int index; + protected int count; + + public BaseItemAddOrRemoveEvent(Container source, Object itemId, + int index, int count) { + super(source); + this.itemId = itemId; + this.index = index; + this.count = count; + } + + public Container getContainer() { + return (Container) getSource(); + } + + public Object getFirstItemId() { + return itemId; + } + + public int getFirstIndex() { + return index; + } + + public int getAffectedItemsCount() { + return count; + } + } + + /** + * An <code>Event</code> object specifying information about the added + * items. + * + * <p> + * This class provides information about the first added item and the number + * of added items. + * </p> + */ + protected static class BaseItemAddEvent extends + BaseItemAddOrRemoveEvent implements + Container.Indexed.ItemAddEvent { + + public BaseItemAddEvent(Container source, Object itemId, int index, + int count) { + super(source, itemId, index, count); + } + + @Override + public int getAddedItemsCount() { + return getAffectedItemsCount(); + } + } + + /** + * An <code>Event</code> object specifying information about the removed + * items. + * + * <p> + * This class provides information about the first removed item and the + * number of removed items. + * </p> + */ + protected static class BaseItemRemoveEvent extends + BaseItemAddOrRemoveEvent implements + Container.Indexed.ItemRemoveEvent { + + public BaseItemRemoveEvent(Container source, Object itemId, + int index, int count) { + super(source, itemId, index, count); + } + + @Override + public int getRemovedItemsCount() { + return getAffectedItemsCount(); + } + } + /** * Get an item even if filtered out. * @@ -898,36 +979,69 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE * Notify item set change listeners that an item has been added to the * container. * - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. - * * @param postion - * position of the added item in the view (if visible) + * position of the added item in the view * @param itemId * id of the added item * @param item * the added item */ protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) { - fireItemSetChange(); + fireItemsAdded(position, itemId, 1); + } + + /** + * Notify item set change listeners that items has been added to the + * container. + * + * @param firstPosition + * position of the first visible added item in the view + * @param firstItemId + * id of the first visible added item + * @param numberOfItems + * the number of visible added items + */ + protected void fireItemsAdded(int firstPosition, ITEMIDTYPE firstItemId, + int numberOfItems) { + BaseItemAddEvent addEvent = new BaseItemAddEvent(this, + firstItemId, firstPosition, numberOfItems); + fireItemSetChange(addEvent); } /** * Notify item set change listeners that an item has been removed from the * container. * - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. + * @param position + * position of the removed item in the view prior to removal * - * @param postion - * position of the removed item in the view prior to removal (if - * was visible) * @param itemId * id of the removed item, of type {@link Object} to satisfy * {@link Container#removeItem(Object)} API */ protected void fireItemRemoved(int position, Object itemId) { - fireItemSetChange(); + fireItemsRemoved(position, itemId, 1); + } + + /** + * Notify item set change listeners that items has been removed from the + * container. + * + * @param firstPosition + * position of the first visible removed item in the view prior + * to removal + * @param firstItemId + * id of the first visible removed item, of type {@link Object} + * to satisfy {@link Container#removeItem(Object)} API + * @param numberOfItems + * the number of removed visible items + * + */ + protected void fireItemsRemoved(int firstPosition, Object firstItemId, + int numberOfItems) { + BaseItemRemoveEvent removeEvent = new BaseItemRemoveEvent(this, + firstItemId, firstPosition, numberOfItems); + fireItemSetChange(removeEvent); } // visible and filtered item identifier lists @@ -946,6 +1060,21 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } /** + * Returns the item id of the first visible item after filtering. 'Null' is + * returned if there is no visible items. + * + * For internal use only. + * + * @return item id of the first visible item + */ + protected ITEMIDTYPE getFirstVisibleItem() { + if (!getVisibleItemIds().isEmpty()) { + return getVisibleItemIds().get(0); + } + return null; + } + + /** * Returns true is the container has active filters. * * @return true if the container is currently filtered diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java index 68960335d7..f9cc4c482a 100644 --- a/server/src/com/vaadin/data/util/IndexedContainer.java +++ b/server/src/com/vaadin/data/util/IndexedContainer.java @@ -226,6 +226,7 @@ public class IndexedContainer extends @Override public boolean removeAllItems() { int origSize = size(); + Object firstItem = getFirstVisibleItem(); internalRemoveAllItems(); @@ -235,7 +236,7 @@ public class IndexedContainer extends // filtered out items were removed or not if (origSize != 0) { // Sends a change event - fireItemSetChange(); + fireItemsRemoved(0, firstItem, origSize); } return true; @@ -620,8 +621,7 @@ public class IndexedContainer extends @Override protected void fireItemAdded(int position, Object itemId, Item item) { if (position >= 0) { - fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, - position)); + super.fireItemAdded(position, itemId, item); } } @@ -1211,4 +1211,5 @@ public class IndexedContainer extends public Collection<Filter> getContainerFilters() { return super.getContainerFilters(); } + } diff --git a/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java b/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java new file mode 100644 index 0000000000..d1cf77c24b --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/AbstractRenderer.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.ui.components.grid; + +import com.vaadin.server.AbstractClientConnector; +import com.vaadin.server.AbstractExtension; + +/** + * An abstract base class for server-side Grid renderers. + * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. This class + * currently extends the AbstractExtension superclass, but this fact should be + * regarded as an implementation detail and subject to change in a future major + * or minor Vaadin revision. + * + * @param <T> + * the type this renderer knows how to present + * + * @since + * @author Vaadin Ltd + */ +public abstract class AbstractRenderer<T> extends AbstractExtension implements + Renderer<T> { + + private final Class<T> presentationType; + + protected AbstractRenderer(Class<T> presentationType) { + this.presentationType = presentationType; + } + + /** + * This method is inherited from AbstractExtension but should never be + * called directly with an AbstractRenderer. + */ + @Deprecated + @Override + protected Class<Grid> getSupportedParentType() { + return Grid.class; + } + + /** + * This method is inherited from AbstractExtension but should never be + * called directly with an AbstractRenderer. + */ + @Deprecated + @Override + protected void extend(AbstractClientConnector target) { + super.extend(target); + } + + @Override + public Class<T> getPresentationType() { + return presentationType; + } + + /** + * Gets the item id for a row key. + * <p> + * A key is used to identify a particular row on both a server and a client. + * This method can be used to get the item id for the row key that the + * client has sent. + * + * @param key + * the row key for which to retrieve an item id + * @return the item id corresponding to {@code key} + */ + protected Object getItemId(String key) { + if (getParent() instanceof Grid) { + Grid grid = (Grid) getParent(); + return grid.getKeyMapper().getItemId(key); + } else { + throw new IllegalStateException( + "Renderers can be used only with Grid"); + } + } +} diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java new file mode 100644 index 0000000000..d365d3e0cc --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -0,0 +1,1298 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui.components.grid; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import 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.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.GridState.SharedSelectionMode; +import com.vaadin.shared.ui.grid.GridStaticCellType; +import com.vaadin.shared.ui.grid.HeightMode; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.ui.grid.SortDirection; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.components.grid.GridFooter.FooterCell; +import com.vaadin.ui.components.grid.GridFooter.FooterRow; +import com.vaadin.ui.components.grid.GridHeader.HeaderCell; +import com.vaadin.ui.components.grid.GridHeader.HeaderRow; +import com.vaadin.ui.components.grid.selection.MultiSelectionModel; +import com.vaadin.ui.components.grid.selection.NoSelectionModel; +import com.vaadin.ui.components.grid.selection.SelectionChangeEvent; +import com.vaadin.ui.components.grid.selection.SelectionChangeListener; +import com.vaadin.ui.components.grid.selection.SelectionChangeNotifier; +import com.vaadin.ui.components.grid.selection.SelectionModel; +import com.vaadin.ui.components.grid.selection.SingleSelectionModel; +import com.vaadin.ui.components.grid.sort.Sort; +import com.vaadin.ui.components.grid.sort.SortOrder; +import com.vaadin.util.ReflectTools; + +/** + * A grid component for displaying tabular data. + * <p> + * Grid is always bound to a {@link Container.Indexed}, but is not a + * {@code Container} of any kind in of itself. The contents of the given + * Container is displayed with the help of {@link Renderer Renderers}. + * + * <h3 id="grid-headers-and-footers">Headers and Footers</h3> + * <p> + * + * + * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3> + * <p> + * Each column has its own {@link Renderer} that displays data into something + * that can be displayed in the browser. That data is first converted with a + * {@link com.vaadin.data.util.converter.Converter Converter} into something + * that the Renderer can process. This can also be an implicit step - if a + * column has a simple data type, like a String, no explicit assignment is + * needed. + * <p> + * Usually a renderer takes some kind of object, and converts it into a + * HTML-formatted string. + * <p> + * <code><pre> + * Grid grid = new Grid(myContainer); + * GridColumn column = grid.getColumn(STRING_DATE_PROPERTY); + * column.setConverter(new StringToDateConverter()); + * column.setRenderer(new MyColorfulDateRenderer()); + * </pre></code> + * + * <h3 id="grid-lazyloading">Lazy Loading</h3> + * <p> + * The data is accessed as it is needed by Grid and not any sooner. In other + * words, if the given Container is huge, but only the first few rows are + * displayed to the user, only those (and a few more, for caching purposes) are + * accessed. + * + * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3> + * <p> + * Grid supports three selection <em>{@link SelectionMode modes}</em> (single, + * multi, none), and comes bundled with one + * <em>{@link SelectionModel model}</em> for each of the modes. The distinction + * between a selection mode and selection model is as follows: a <em>mode</em> + * essentially says whether you can have one, many or no rows selected. The + * model, however, has the behavioral details of each. A single selection model + * may require that the user deselects one row before selecting another one. A + * variant of a multiselect might have a configurable maximum of rows that may + * be selected. And so on. + * <p> + * <code><pre> + * Grid grid = new Grid(myContainer); + * + * // uses the bundled SingleSelectionModel class + * grid.setSelectionMode(SelectionMode.SINGLE); + * + * // changes the behavior to a custom selection model + * grid.setSelectionModel(new MyTwoSelectionModel()); + * </pre></code> + * + * @since + * @author Vaadin Ltd + */ +public class Grid extends AbstractComponent implements SelectionChangeNotifier, + HasComponents { + + /** + * Selection modes representing built-in {@link SelectionModel + * SelectionModels} that come bundled with {@link Grid}. + * <p> + * Passing one of these enums into + * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling + * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in + * implementations of {@link SelectionModel}. + * + * @see Grid#setSelectionMode(SelectionMode) + * @see Grid#setSelectionModel(SelectionModel) + */ + public enum SelectionMode { + /** A SelectionMode that maps to {@link SingleSelectionModel} */ + SINGLE { + @Override + protected SelectionModel createModel() { + return new SingleSelectionModel(); + } + + }, + + /** A SelectionMode that maps to {@link MultiSelectionModel} */ + MULTI { + @Override + protected SelectionModel createModel() { + return new MultiSelectionModel(); + } + }, + + /** A SelectionMode that maps to {@link NoSelectionModel} */ + NONE { + @Override + protected SelectionModel createModel() { + return new NoSelectionModel(); + } + }; + + protected abstract SelectionModel createModel(); + } + + /** + * The data source attached to the grid + */ + private Container.Indexed datasource; + + /** + * Property id to column instance mapping + */ + private final Map<Object, GridColumn> columns = new HashMap<Object, GridColumn>(); + + /** + * Key generator for column server-to-client communication + */ + private final KeyMapper<Object> columnKeys = new KeyMapper<Object>(); + + /** + * The current sort order + */ + private final List<SortOrder> sortOrder = new ArrayList<SortOrder>(); + + /** + * Property listener for listening to changes in data source properties. + */ + private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() { + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + Collection<?> properties = new HashSet<Object>(event.getContainer() + .getContainerPropertyIds()); + + // Cleanup columns that are no longer in grid + List<Object> removedColumns = new LinkedList<Object>(); + for (Object columnId : columns.keySet()) { + if (!properties.contains(columnId)) { + removedColumns.add(columnId); + } + } + for (Object columnId : removedColumns) { + GridColumn column = columns.remove(columnId); + columnKeys.remove(columnId); + getState().columns.remove(column.getState()); + removeExtension(column.getRenderer()); + } + datasourceExtension.propertiesRemoved(removedColumns); + + // Add new columns + HashSet<Object> addedPropertyIds = new HashSet<Object>(); + for (Object propertyId : properties) { + if (!columns.containsKey(propertyId)) { + appendColumn(propertyId); + addedPropertyIds.add(propertyId); + } + } + datasourceExtension.propertiesAdded(addedPropertyIds); + + Object frozenPropertyId = columnKeys + .get(getState(false).lastFrozenColumnId); + if (!columns.containsKey(frozenPropertyId)) { + setLastFrozenPropertyId(null); + } + } + }; + + private RpcDataProviderExtension datasourceExtension; + + /** + * The selection model that is currently in use. Never <code>null</code> + * after the constructor has been run. + */ + 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 final GridHeader header = new GridHeader(this); + private final GridFooter footer = new GridFooter(this); + + private static final Method SELECTION_CHANGE_METHOD = ReflectTools + .findMethod(SelectionChangeListener.class, "selectionChange", + SelectionChangeEvent.class); + + private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools + .findMethod(SortOrderChangeListener.class, "sortOrderChange", + SortOrderChangeEvent.class); + + /** + * Creates a new Grid using the given datasource. + * + * @param datasource + * the data source for the grid + */ + 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()) { + getKeyMapper().unpin(removedItemId); + } + + for (Object addedItemId : event.getAdded()) { + getKeyMapper().pin(addedItemId); + } + + List<String> keys = getKeyMapper().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>( + getKeyMapper().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 (!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); + } + } + + 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); + } + } + } + + @Override + public void sort(String[] columnIds, SortDirection[] directions) { + assert columnIds.length == directions.length; + + List<SortOrder> order = new ArrayList<SortOrder>( + columnIds.length); + for (int i = 0; i < columnIds.length; i++) { + Object propertyId = getPropertyIdByColumnId(columnIds[i]); + order.add(new SortOrder(propertyId, directions[i])); + } + + setSortOrder(order); + } + }); + } + + /** + * Sets the grid data source. + * + * @param container + * The container data source. Cannot be null. + * @throws IllegalArgumentException + * if the data source is null + */ + public void setContainerDataSource(Container.Indexed container) { + + if (container == null) { + throw new IllegalArgumentException( + "Cannot set the datasource to null"); + } + if (datasource == container) { + return; + } + + // Remove old listeners + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .removePropertySetChangeListener(propertyListener); + } + + if (datasourceExtension != null) { + removeExtension(datasourceExtension); + } + + datasource = container; + + // + // Adjust sort order + // + + if (container instanceof Container.Sortable) { + + // If the container is sortable, go through the current sort order + // and match each item to the sortable properties of the new + // container. If the new container does not support an item in the + // current sort order, that item is removed from the current sort + // order list. + Collection<?> sortableProps = ((Container.Sortable) getContainerDatasource()) + .getSortableContainerPropertyIds(); + + Iterator<SortOrder> i = sortOrder.iterator(); + while (i.hasNext()) { + if (!sortableProps.contains(i.next().getPropertyId())) { + i.remove(); + } + } + + sort(); + } else { + + // If the new container is not sortable, we'll just re-set the sort + // order altogether. + clearSortOrder(); + } + + datasourceExtension = new RpcDataProviderExtension(container); + datasourceExtension.extend(this); + + /* + * selectionModel == null when the invocation comes from the + * constructor. + */ + if (selectionModel != null) { + selectionModel.reset(); + } + + // Listen to changes in properties and remove columns if needed + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .addPropertySetChangeListener(propertyListener); + } + /* + * activeRowHandler will be updated by the client-side request that + * occurs on container change - no need to actively re-insert any + * ValueChangeListeners at this point. + */ + + getState().columns.clear(); + setLastFrozenPropertyId(null); + + // Add columns + HeaderRow row = getHeader().getDefaultRow(); + for (Object propertyId : datasource.getContainerPropertyIds()) { + if (!columns.containsKey(propertyId)) { + GridColumn column = appendColumn(propertyId); + + // Initial sorting is defined by container + if (datasource instanceof Sortable) { + column.setSortable(((Sortable) datasource) + .getSortableContainerPropertyIds().contains( + propertyId)); + } + + // Add by default property id as column header + row.getCell(propertyId).setText(String.valueOf(propertyId)); + } + } + } + + /** + * Returns the grid data source. + * + * @return the container data source of the grid + */ + public Container.Indexed getContainerDatasource() { + return datasource; + } + + /** + * Returns a column based on the property id + * + * @param propertyId + * the property id of the column + * @return the column or <code>null</code> if not found + */ + public GridColumn getColumn(Object propertyId) { + return columns.get(propertyId); + } + + /** + * Used internally by the {@link Grid} to get a {@link GridColumn} by + * referencing its generated state id. Also used by {@link GridColumn} to + * verify if it has been detached from the {@link Grid}. + * + * @param columnId + * the client id generated for the column when the column is + * added to the grid + * @return the column with the id or <code>null</code> if not found + */ + GridColumn getColumnByColumnId(String columnId) { + Object propertyId = getPropertyIdByColumnId(columnId); + return getColumn(propertyId); + } + + /** + * Used internally by the {@link Grid} to get a property id by referencing + * the columns generated state id. + * + * @param columnId + * The state id of the column + * @return The column instance or null if not found + */ + Object getPropertyIdByColumnId(String columnId) { + return columnKeys.get(columnId); + } + + @Override + protected GridState getState() { + return (GridState) super.getState(); + } + + @Override + protected GridState getState(boolean markAsDirty) { + return (GridState) super.getState(markAsDirty); + } + + /** + * Creates a new column based on a property id and appends it as the last + * column. + * + * @param datasourcePropertyId + * The property id of a property in the datasource + */ + private GridColumn appendColumn(Object datasourcePropertyId) { + if (datasourcePropertyId == null) { + throw new IllegalArgumentException("Property id cannot be null"); + } + assert datasource.getContainerPropertyIds().contains( + datasourcePropertyId) : "Datasource should contain the property id"; + + GridColumnState columnState = new GridColumnState(); + columnState.id = columnKeys.key(datasourcePropertyId); + getState().columns.add(columnState); + + for (int i = 0; i < getHeader().getRowCount(); ++i) { + getHeader().getRow(i).addCell(datasourcePropertyId); + } + + for (int i = 0; i < getFooter().getRowCount(); ++i) { + getFooter().getRow(i).addCell(datasourcePropertyId); + } + + GridColumn column = new GridColumn(this, columnState); + columns.put(datasourcePropertyId, column); + + return column; + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + * <p> + * All columns up to and including the given column will be frozen in place + * when the grid is scrolled sideways. + * + * @param lastFrozenColumn + * the rightmost column to freeze, or <code>null</code> to not + * have any columns frozen + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + void setLastFrozenColumn(GridColumn lastFrozenColumn) { + /* + * TODO: If and when Grid supports column reordering or insertion of + * columns before other columns, make sure to mention that adding + * columns before lastFrozenColumn will change the frozen column count + */ + + if (lastFrozenColumn == null) { + getState().lastFrozenColumnId = null; + } else if (columns.containsValue(lastFrozenColumn)) { + getState().lastFrozenColumnId = lastFrozenColumn.getState().id; + } else { + throw new IllegalArgumentException( + "The given column isn't attached to this grid"); + } + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + * <p> + * All columns up to and including the indicated property will be frozen in + * place when the grid is scrolled sideways. + * <p> + * <em>Note:</em> If the container used by this grid supports a propertyId + * <code>null</code>, it can never be defined as the last frozen column, as + * a <code>null</code> parameter will always reset the frozen columns in + * Grid. + * + * @param propertyId + * the property id corresponding to the column that should be the + * last frozen column, or <code>null</code> to not have any + * columns frozen. + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + public void setLastFrozenPropertyId(Object propertyId) { + final GridColumn column; + if (propertyId == null) { + column = null; + } else { + column = getColumn(propertyId); + if (column == null) { + throw new IllegalArgumentException( + "property id does not exist."); + } + } + setLastFrozenColumn(column); + } + + /** + * Gets the rightmost frozen column in the grid. + * <p> + * <em>Note:</em> Most often, this method returns the very value set with + * {@link #setLastFrozenPropertyId(Object)}. This value, however, can be + * reset to <code>null</code> if the column is detached from this grid. + * + * @return the rightmost frozen column in the grid, or <code>null</code> if + * no columns are frozen. + */ + public Object getLastFrozenPropertyId() { + return columnKeys.get(getState().lastFrozenColumnId); + } + + /** + * Scrolls to a certain item, using {@link ScrollDestination#ANY}. + * + * @param itemId + * id of item to scroll to. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollTo(Object itemId) throws IllegalArgumentException { + scrollTo(itemId, ScrollDestination.ANY); + } + + /** + * Scrolls to a certain item, using user-specified scroll destination. + * + * @param itemId + * id of item to scroll to. + * @param destination + * value specifying desired position of scrolled-to row. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollTo(Object itemId, ScrollDestination destination) + throws IllegalArgumentException { + + int row = datasource.indexOfId(itemId); + + if (row == -1) { + throw new IllegalArgumentException( + "Item with specified ID does not exist in data source"); + } + + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToRow(row, destination); + } + + /** + * Scrolls to the beginning of the first data row. + */ + public void scrollToStart() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToStart(); + } + + /** + * Scrolls to the end of the last data row. + */ + public void scrollToEnd() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToEnd(); + } + + /** + * Sets the number of rows that should be visible in Grid's body, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * <p> + * If Grid is currently not in {@link HeightMode#ROW}, the given value is + * remembered, and applied once the mode is applied. + * + * @param rows + * The height in terms of number of rows displayed in Grid's + * body. If Grid doesn't contain enough rows, white space is + * displayed instead. If <code>null</code> is given, then Grid's + * height is undefined + * @throws IllegalArgumentException + * if {@code rows} is zero or less + * @throws IllegalArgumentException + * if {@code rows} is {@link Double#isInifinite(double) + * infinite} + * @throws IllegalArgumentException + * if {@code rows} is {@link Double#isNaN(double) NaN} + */ + public void setHeightByRows(double rows) { + if (rows <= 0.0d) { + throw new IllegalArgumentException( + "More than zero rows must be shown."); + } else if (Double.isInfinite(rows)) { + throw new IllegalArgumentException( + "Grid doesn't support infinite heights"); + } else if (Double.isNaN(rows)) { + throw new IllegalArgumentException("NaN is not a valid row count"); + } + + getState().heightByRows = rows; + } + + /** + * Gets the amount of rows in Grid's body that are shown, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * + * @return the amount of rows that are being shown in Grid's body + * @see #setHeightByRows(double) + */ + public double getHeightByRows() { + return getState(false).heightByRows; + } + + /** + * {@inheritDoc} + * <p> + * <em>Note:</em> This method will change the widget's size in the browser + * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}. + * + * @see #setHeightMode(HeightMode) + */ + @Override + public void setHeight(float height, Unit unit) { + super.setHeight(height, unit); + } + + /** + * Defines the mode in which the Grid widget's height is calculated. + * <p> + * If {@link HeightMode#CSS} is given, Grid will respect the values given + * via a {@code setHeight}-method, and behave as a traditional Component. + * <p> + * If {@link HeightMode#ROW} is given, Grid will make sure that the body + * will display as many rows as {@link #getHeightByRows()} defines. + * <em>Note:</em> If headers/footers are inserted or removed, the widget + * will resize itself to still display the required amount of rows in its + * body. It also takes the horizontal scrollbar into account. + * + * @param heightMode + * the mode in to which Grid should be set + */ + public void setHeightMode(HeightMode heightMode) { + /* + * This method is a workaround for the fact that Vaadin re-applies + * widget dimensions (height/width) on each state change event. The + * original design was to have setHeight an setHeightByRow be equals, + * and whichever was called the latest was considered in effect. + * + * But, because of Vaadin always calling setHeight on the widget, this + * approach doesn't work. + */ + + getState().heightMode = heightMode; + } + + /** + * Returns the current {@link HeightMode} the Grid is in. + * <p> + * Defaults to {@link HeightMode#CSS}. + * + * @return the current HeightMode + */ + public HeightMode getHeightMode() { + return getState(false).heightMode; + } + + /* Selection related methods: */ + + /** + * Takes a new {@link SelectionModel} into use. + * <p> + * The SelectionModel that is previously in use will have all its items + * deselected. + * <p> + * If the given SelectionModel is already in use, this method does nothing. + * + * @param selectionModel + * the new SelectionModel to use + * @throws IllegalArgumentException + * if {@code selectionModel} is <code>null</code> + */ + public void setSelectionModel(SelectionModel selectionModel) + throws IllegalArgumentException { + if (selectionModel == null) { + throw new IllegalArgumentException( + "Selection model may not be null"); + } + + if (this.selectionModel != selectionModel) { + // this.selectionModel is null on init + if (this.selectionModel != null) { + this.selectionModel.reset(); + this.selectionModel.setGrid(null); + } + + this.selectionModel = selectionModel; + this.selectionModel.setGrid(this); + this.selectionModel.reset(); + + if (selectionModel.getClass().equals(SingleSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.SINGLE; + } else if (selectionModel.getClass().equals( + MultiSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.MULTI; + } else if (selectionModel.getClass().equals(NoSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.NONE; + } else { + throw new UnsupportedOperationException("Grid currently " + + "supports only its own bundled selection models"); + } + } + } + + /** + * Returns the currently used {@link SelectionModel}. + * + * @return the currently used SelectionModel + */ + public SelectionModel getSelectionModel() { + return selectionModel; + } + + /** + * Changes the Grid's selection mode. + * <p> + * Grid supports three selection modes: multiselect, single select and no + * selection, and this is a conveniency method for choosing between one of + * them. + * <P> + * Technically, this method is a shortcut that can be used instead of + * calling {@code setSelectionModel} with a specific SelectionModel + * instance. Grid comes with three built-in SelectionModel classes, and the + * {@link SelectionMode} enum represents each of them. + * <p> + * Essentially, the two following method calls are equivalent: + * <p> + * <code><pre> + * grid.setSelectionMode(SelectionMode.MULTI); + * grid.setSelectionModel(new MultiSelectionMode()); + * </pre></code> + * + * + * @param selectionMode + * the selection mode to switch to + * @return The {@link SelectionModel} instance that was taken into use + * @throws IllegalArgumentException + * if {@code selectionMode} is <code>null</code> + * @see SelectionModel + */ + public SelectionModel setSelectionMode(final SelectionMode selectionMode) + throws IllegalArgumentException { + if (selectionMode == null) { + throw new IllegalArgumentException("selection mode may not be null"); + } + final SelectionModel newSelectionModel = selectionMode.createModel(); + setSelectionModel(newSelectionModel); + return newSelectionModel; + } + + /** + * Checks whether an item is selected or not. + * + * @param itemId + * the item id to check for + * @return <code>true</code> iff the item is selected + */ + // keep this javadoc in sync with SelectionModel.isSelected + public boolean isSelected(Object itemId) { + return selectionModel.isSelected(itemId); + } + + /** + * Returns a collection of all the currently selected itemIds. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. + * + * @return a collection of all the currently selected itemIds + */ + // keep this javadoc in sync with SelectionModel.getSelectedRows + public Collection<Object> getSelectedRows() { + return getSelectionModel().getSelectedRows(); + } + + /** + * Gets the item id of the currently selected item. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} is supported. + * + * @return the item id of the currently selected item, or <code>null</code> + * if nothing is selected + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} is not an instance of + * {@link SelectionModel.Single} + */ + // keep this javadoc in sync with SelectionModel.Single.getSelectedRow + public Object getSelectedRow() throws IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + return ((SelectionModel.Single) selectionModel).getSelectedRow(); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'getSelectedRow' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")"); + } + } + + /** + * Marks an item as selected. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} are + * supported. + * + * + * @param itemIds + * the itemId to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalArgumentException + * if the {@code itemId} doesn't exist in the currently active + * Container + * @throws IllegalStateException + * if the selection was illegal. One such reason might be that + * the implementation already had an item selected, and that + * needs to be explicitly deselected before re-selecting + * something + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} does not implement + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} + */ + // keep this javadoc in sync with SelectionModel.Single.select + public boolean select(Object itemId) throws IllegalArgumentException, + IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + return ((SelectionModel.Single) selectionModel).select(itemId); + } else if (selectionModel instanceof SelectionModel.Multi) { + return ((SelectionModel.Multi) selectionModel).select(itemId); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'select' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")."); + } + } + + /** + * Marks an item as deselected. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are + * supported. + * + * @param itemId + * the itemId to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalArgumentException + * if the {@code itemId} doesn't exist in the currently active + * Container + * @throws IllegalStateException + * if the deselection was illegal. One such reason might be that + * the implementation already had an item selected, and that + * needs to be explicitly deselected before re-selecting + * something + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} does not implement + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} + */ + // keep this javadoc in sync with SelectionModel.Single.deselect + public boolean deselect(Object itemId) throws IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + return ((SelectionModel.Single) selectionModel).deselect(itemId); + } else if (selectionModel instanceof SelectionModel.Multi) { + return ((SelectionModel.Multi) selectionModel).deselect(itemId); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'deselect' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")."); + } + } + + /** + * Fires a selection change event. + * <p> + * <strong>Note:</strong> This is not a method that should be called by + * application logic. This method is publicly accessible only so that + * {@link SelectionModel SelectionModels} would be able to inform Grid of + * these events. + * + * @param addedSelections + * the selections that were added by this event + * @param removedSelections + * the selections that were removed by this event + */ + public void fireSelectionChangeEvent(Collection<Object> oldSelection, + Collection<Object> newSelection) { + fireEvent(new SelectionChangeEvent(this, oldSelection, newSelection)); + } + + @Override + public void addSelectionChangeListener(SelectionChangeListener listener) { + addListener(SelectionChangeEvent.class, listener, + SELECTION_CHANGE_METHOD); + } + + @Override + public void removeSelectionChangeListener(SelectionChangeListener listener) { + removeListener(SelectionChangeEvent.class, listener, + SELECTION_CHANGE_METHOD); + } + + /** + * Gets the + * {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper + * DataProviderKeyMapper} being used by the data source. + * + * @return the key mapper being used by the data source + */ + DataProviderKeyMapper getKeyMapper() { + return datasourceExtension.getKeyMapper(); + } + + /** + * Adds a renderer to this grid's connector hierarchy. + * + * @param renderer + * the renderer to add + */ + void addRenderer(Renderer<?> renderer) { + addExtension(renderer); + } + + /** + * Sets the current sort order using the fluid Sort API. Read the + * documentation for {@link Sort} for more information. + * + * @param s + * a sort instance + */ + public void sort(Sort s) { + setSortOrder(s.build()); + } + + /** + * Sort this Grid in ascending order by a specified property. + * + * @param propertyId + * a property ID + */ + public void sort(Object propertyId) { + sort(propertyId, SortDirection.ASCENDING); + } + + /** + * Sort this Grid in user-specified {@link SortOrder} by a property. + * + * @param propertyId + * a property ID + * @param direction + * a sort order value (ascending/descending) + */ + public void sort(Object propertyId, SortDirection direction) { + sort(Sort.by(propertyId, direction)); + } + + /** + * Clear the current sort order, and re-sort the grid. + */ + public void clearSortOrder() { + sortOrder.clear(); + sort(); + } + + /** + * Sets the sort order to use. This method throws + * {@link IllegalStateException} if the attached container is not a + * {@link Container.Sortable}, and {@link IllegalArgumentException} if a + * property in the list is not recognized by the container, or if the + * 'order' parameter is null. + * + * @param order + * a sort order list. + */ + public void setSortOrder(List<SortOrder> order) { + if (!(getContainerDatasource() instanceof Container.Sortable)) { + throw new IllegalStateException( + "Attached container is not sortable (does not implement Container.Sortable)"); + } + + if (order == null) { + throw new IllegalArgumentException("Order list may not be null!"); + } + + sortOrder.clear(); + + Collection<?> sortableProps = ((Container.Sortable) getContainerDatasource()) + .getSortableContainerPropertyIds(); + + for (SortOrder o : order) { + if (!sortableProps.contains(o.getPropertyId())) { + throw new IllegalArgumentException( + "Property " + + o.getPropertyId() + + " does not exist or is not sortable in the current container"); + } + } + + sortOrder.addAll(order); + sort(); + } + + /** + * Get the current sort order list. + * + * @return a sort order list + */ + public List<SortOrder> getSortOrder() { + return Collections.unmodifiableList(sortOrder); + } + + /** + * Apply sorting to data source. + */ + private void sort() { + + Container c = getContainerDatasource(); + if (c instanceof Container.Sortable) { + Container.Sortable cs = (Container.Sortable) c; + + final int items = sortOrder.size(); + Object[] propertyIds = new Object[items]; + boolean[] directions = new boolean[items]; + + String[] columnKeys = new String[items]; + SortDirection[] stateDirs = new SortDirection[items]; + + for (int i = 0; i < items; ++i) { + SortOrder order = sortOrder.get(i); + + columnKeys[i] = this.columnKeys.key(order.getPropertyId()); + stateDirs[i] = order.getDirection(); + + propertyIds[i] = order.getPropertyId(); + switch (order.getDirection()) { + case ASCENDING: + directions[i] = true; + break; + case DESCENDING: + directions[i] = false; + break; + default: + throw new IllegalArgumentException("getDirection() of " + + order + " returned an unexpected value"); + } + } + + cs.sort(propertyIds, directions); + + fireEvent(new SortOrderChangeEvent(this, new ArrayList<SortOrder>( + sortOrder))); + + getState().sortColumns = columnKeys; + getState(false).sortDirs = stateDirs; + } else { + throw new IllegalStateException( + "Container is not sortable (does not implement Container.Sortable)"); + } + } + + /** + * Adds a sort order change listener that gets notified when the sort order + * changes. + * + * @param listener + * the sort order change listener to add + */ + public void addSortOrderChangeListener(SortOrderChangeListener listener) { + addListener(SortOrderChangeEvent.class, listener, + SORT_ORDER_CHANGE_METHOD); + } + + /** + * Removes a sort order change listener previously added using + * {@link #addSortOrderChangeListener(SortOrderChangeListener)}. + * + * @param listener + * the sort order change listener to remove + */ + public void removeSortOrderChangeListener(SortOrderChangeListener listener) { + removeListener(SortOrderChangeEvent.class, listener, + SORT_ORDER_CHANGE_METHOD); + } + + /** + * Returns the header section of this grid. The default header contains a + * single row displaying the column captions. + * + * @return the header + */ + public GridHeader getHeader() { + return header; + } + + /** + * Returns the footer section of this grid. The default header contains a + * single row displaying the column captions. + * + * @return the footer + */ + public GridFooter getFooter() { + return footer; + } + + @Override + public Iterator<Component> iterator() { + List<Component> componentList = new ArrayList<Component>(); + + GridHeader header = getHeader(); + for (int i = 0; i < header.getRowCount(); ++i) { + HeaderRow row = header.getRow(i); + for (Object propId : datasource.getContainerPropertyIds()) { + HeaderCell cell = row.getCell(propId); + if (cell.getCellState().type == GridStaticCellType.WIDGET) { + componentList.add(cell.getComponent()); + } + } + } + + GridFooter footer = getFooter(); + for (int i = 0; i < footer.getRowCount(); ++i) { + FooterRow row = footer.getRow(i); + for (Object propId : datasource.getContainerPropertyIds()) { + FooterCell cell = row.getCell(propId); + if (cell.getCellState().type == GridStaticCellType.WIDGET) { + componentList.add(cell.getComponent()); + } + } + } + + return componentList.iterator(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java new file mode 100644 index 0000000000..0ef805eb2e --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java @@ -0,0 +1,427 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui.components.grid; + +import java.io.Serializable; + +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; +import com.vaadin.ui.UI; +import com.vaadin.ui.components.grid.renderers.TextRenderer; + +/** + * A column in the grid. Can be obtained by calling + * {@link Grid#getColumn(Object propertyId)}. + * + * @since + * @author Vaadin Ltd + */ +public class GridColumn implements Serializable { + + /** + * The state of the column shared to the client + */ + private final GridColumnState state; + + /** + * The grid this column is associated with + */ + private final Grid grid; + + private Converter<?, Object> converter; + + /** + * A check for allowing the {@link #GridColumn(Grid, GridColumnState) + * constructor} to call {@link #setConverter(Converter)} with a + * <code>null</code>, even if model and renderer aren't compatible. + */ + private boolean isFirstConverterAssignment = true; + + /** + * Internally used constructor. + * + * @param grid + * The grid this column belongs to. Should not be null. + * @param state + * the shared state of this column + */ + GridColumn(Grid grid, GridColumnState state) { + this.grid = grid; + this.state = state; + internalSetRenderer(new TextRenderer()); + } + + /** + * Returns the serializable state of this column that is sent to the client + * side connector. + * + * @return the internal state of the column + */ + GridColumnState getState() { + return state; + } + + /** + * Returns the caption of the header. By default the header caption is the + * property id of the column. + * + * @return the text in the default row of header, null if no default row + * + * @throws IllegalStateException + * if the column no longer is attached to the grid + */ + @Deprecated + public String getHeaderCaption() throws IllegalStateException { + checkColumnIsAttached(); + return state.header; + } + + /** + * Sets the caption of the header. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + @Deprecated + public void setHeaderCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + state.header = caption; + } + + /** + * Returns the caption of the footer. By default the captions are + * <code>null</code>. + * + * @return the text in the footer + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + @Deprecated + public String getFooterCaption() throws IllegalStateException { + checkColumnIsAttached(); + return getFooterCellState().text; + } + + /** + * Sets the caption of the footer. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + @Deprecated + public void setFooterCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + getFooterCellState().text = caption; + state.footer = caption; + grid.markAsDirty(); + } + + private CellState getFooterCellState() { + int index = grid.getState().columns.indexOf(state); + return grid.getState().footer.rows.get(0).cells.get(index); + } + + /** + * Returns the width (in pixels). By default a column is 100px wide. + * + * @return the width in pixels of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public int getWidth() throws IllegalStateException { + checkColumnIsAttached(); + return state.width; + } + + /** + * Sets the width (in pixels). + * + * @param pixelWidth + * the new pixel width of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @throws IllegalArgumentException + * thrown if pixel width is less than zero + */ + public void setWidth(int pixelWidth) throws IllegalStateException, + IllegalArgumentException { + checkColumnIsAttached(); + if (pixelWidth < 0) { + throw new IllegalArgumentException( + "Pixel width should be greated than 0"); + } + state.width = pixelWidth; + grid.markAsDirty(); + } + + /** + * Marks the column width as undefined meaning that the grid is free to + * resize the column based on the cell contents and available space in the + * grid. + */ + public void setWidthUndefined() { + checkColumnIsAttached(); + state.width = -1; + grid.markAsDirty(); + } + + /** + * Is this column visible in the grid. By default all columns are visible. + * + * @return <code>true</code> if the column is visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public boolean isVisible() throws IllegalStateException { + checkColumnIsAttached(); + return state.visible; + } + + /** + * Set the visibility of this column + * + * @param visible + * is the column visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setVisible(boolean visible) throws IllegalStateException { + checkColumnIsAttached(); + state.visible = visible; + grid.markAsDirty(); + } + + /** + * Checks if column is attached and throws an {@link IllegalStateException} + * if it is not + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + protected void checkColumnIsAttached() throws IllegalStateException { + if (grid.getColumnByColumnId(state.id) == null) { + throw new IllegalStateException("Column no longer exists."); + } + } + + /** + * Sets this column as the last frozen column in its grid. + * + * @throws IllegalArgumentException + * if the column is no longer attached to any grid + * @see Grid#setLastFrozenColumn(GridColumn) + */ + public void setLastFrozenColumn() { + checkColumnIsAttached(); + grid.setLastFrozenColumn(this); + } + + /** + * Sets the renderer for this column. + * <p> + * If a suitable converter isn't defined explicitly, the session converter + * factory is used to find a compatible converter. + * + * @param renderer + * the renderer to use + * @throws IllegalArgumentException + * if no compatible converter could be found + * + * @see VaadinSession#getConverterFactory() + * @see ConverterUtil#getConverter(Class, Class, VaadinSession) + * @see #setConverter(Converter) + */ + public void setRenderer(Renderer<?> renderer) { + if (!internalSetRenderer(renderer)) { + throw new IllegalArgumentException( + "Could not find a converter for converting from the model type " + + getModelType() + + " to the renderer presentation type " + + renderer.getPresentationType()); + } + } + + /** + * Sets the renderer for this column and the converter used to convert from + * the property value type to the renderer presentation type. + * + * @param renderer + * the renderer to use, cannot be null + * @param converter + * the converter to use + * + * @throws IllegalArgumentException + * if the renderer is already associated with a grid column + */ + public <T> void setRenderer(Renderer<T> renderer, + Converter<? extends T, ?> converter) { + if (renderer.getParent() != null) { + throw new IllegalArgumentException( + "Cannot set a renderer that is already connected to a grid column"); + } + + if (getRenderer() != null) { + grid.removeExtension(getRenderer()); + } + + grid.addRenderer(renderer); + state.rendererConnector = renderer; + setConverter(converter); + } + + /** + * Sets the converter used to convert from the property value type to the + * renderer presentation type. + * + * @param converter + * the converter to use, or {@code null} to not use any + * converters + * @throws IllegalArgumentException + * if the types are not compatible + */ + public void setConverter(Converter<?, ?> converter) + throws IllegalArgumentException { + Class<?> modelType = getModelType(); + if (converter != null) { + if (!converter.getModelType().isAssignableFrom(modelType)) { + throw new IllegalArgumentException("The converter model type " + + converter.getModelType() + + " is not compatible with the property type " + + modelType); + + } else if (!getRenderer().getPresentationType().isAssignableFrom( + converter.getPresentationType())) { + throw new IllegalArgumentException( + "The converter presentation type " + + converter.getPresentationType() + + " is not compatible with the renderer presentation type " + + getRenderer().getPresentationType()); + } + } + + else { + /* + * Since the converter is null (i.e. will be removed), we need to + * know that the renderer and model are compatible. If not, we can't + * allow for this to happen. + * + * The constructor is allowed to call this method with null without + * any compatibility checks, therefore we have a special case for + * it. + */ + + Class<?> rendererPresentationType = getRenderer() + .getPresentationType(); + if (!isFirstConverterAssignment + && !rendererPresentationType.isAssignableFrom(modelType)) { + throw new IllegalArgumentException("Cannot remove converter, " + + "as renderer's presentation type " + + rendererPresentationType.getName() + " and column's " + + "model " + modelType.getName() + " type aren't " + + "directly with each other"); + } + } + + isFirstConverterAssignment = false; + + @SuppressWarnings("unchecked") + Converter<?, Object> castConverter = (Converter<?, Object>) converter; + this.converter = castConverter; + } + + /** + * Returns the renderer instance used by this column. + * + * @return the renderer + */ + public Renderer<?> getRenderer() { + return (Renderer<?>) getState().rendererConnector; + } + + /** + * Returns the converter instance used by this column. + * + * @return the converter + */ + public Converter<?, ?> getConverter() { + return converter; + } + + private <T> boolean internalSetRenderer(Renderer<T> renderer) { + + Converter<? extends T, ?> converter; + if (isCompatibleWithProperty(renderer, getConverter())) { + // Use the existing converter (possibly none) if types compatible + converter = (Converter<? extends T, ?>) getConverter(); + } else { + converter = ConverterUtil.getConverter( + renderer.getPresentationType(), getModelType(), + getSession()); + } + setRenderer(renderer, converter); + return isCompatibleWithProperty(renderer, converter); + } + + private VaadinSession getSession() { + UI ui = grid.getUI(); + return ui != null ? ui.getSession() : null; + } + + private boolean isCompatibleWithProperty(Renderer<?> renderer, + Converter<?, ?> converter) { + Class<?> type; + if (converter == null) { + type = getModelType(); + } else { + type = converter.getPresentationType(); + } + return renderer.getPresentationType().isAssignableFrom(type); + } + + private Class<?> getModelType() { + return grid.getContainerDatasource().getType( + grid.getPropertyIdByColumnId(state.id)); + } + + /** + * Should sorting controls be available for the column + * + * @param sortable + * <code>true</code> if the sorting controls should be visible. + */ + public void setSortable(boolean sortable) { + checkColumnIsAttached(); + state.sortable = sortable; + grid.markAsDirty(); + } + + /** + * Are the sorting controls visible in the column header + */ + public boolean isSortable() { + return state.sortable; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridFooter.java b/server/src/com/vaadin/ui/components/grid/GridFooter.java new file mode 100644 index 0000000000..0a28a481cf --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridFooter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import com.vaadin.shared.ui.grid.GridStaticSectionState; + +/** + * Represents the footer section of a Grid. By default Footer is not visible. + * + * @since + * @author Vaadin Ltd + */ +public class GridFooter extends GridStaticSection<GridFooter.FooterRow> { + + public class FooterRow extends GridStaticSection.StaticRow<FooterCell> { + + protected FooterRow(GridStaticSection<?> section) { + super(section); + } + + @Override + protected FooterCell createCell() { + return new FooterCell(this); + } + + } + + public class FooterCell extends GridStaticSection.StaticCell { + + protected FooterCell(FooterRow row) { + super(row); + } + } + + private final GridStaticSectionState footerState = new GridStaticSectionState(); + + protected GridFooter(Grid grid) { + this.grid = grid; + grid.getState(true).footer = footerState; + setVisible(false); + } + + @Override + protected GridStaticSectionState getSectionState() { + return footerState; + } + + @Override + protected FooterRow createRow() { + return new FooterRow(this); + } + +} diff --git a/server/src/com/vaadin/ui/components/grid/GridHeader.java b/server/src/com/vaadin/ui/components/grid/GridHeader.java new file mode 100644 index 0000000000..9d7ec24a97 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridHeader.java @@ -0,0 +1,124 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import com.vaadin.shared.ui.grid.GridStaticSectionState; + +/** + * Represents the header section of a Grid. + * + * @since + * @author Vaadin Ltd + */ +public class GridHeader extends GridStaticSection<GridHeader.HeaderRow> { + + public class HeaderRow extends GridStaticSection.StaticRow<HeaderCell> { + + protected HeaderRow(GridStaticSection<?> section) { + super(section); + } + + private void setDefaultRow(boolean value) { + getRowState().defaultRow = value; + } + + @Override + protected HeaderCell createCell() { + return new HeaderCell(this); + } + } + + public class HeaderCell extends GridStaticSection.StaticCell { + + protected HeaderCell(HeaderRow row) { + super(row); + } + } + + private HeaderRow defaultRow = null; + private final GridStaticSectionState headerState = new GridStaticSectionState(); + + protected GridHeader(Grid grid) { + this.grid = grid; + grid.getState(true).header = headerState; + HeaderRow row = createRow(); + rows.add(row); + setDefaultRow(row); + getSectionState().rows.add(row.getRowState()); + } + + /** + * Sets the default row of this header. The default row is a special header + * row providing a user interface for sorting columns. + * + * @param row + * the new default row, or null for no default row + * + * @throws IllegalArgumentException + * this header does not contain the row + */ + public void setDefaultRow(HeaderRow row) { + if (row == defaultRow) { + return; + } + + if (row != null && !rows.contains(row)) { + throw new IllegalArgumentException( + "Cannot set a default row that does not exist in the section"); + } + + if (defaultRow != null) { + defaultRow.setDefaultRow(false); + } + + if (row != null) { + row.setDefaultRow(true); + } + + defaultRow = row; + markAsDirty(); + } + + /** + * Returns the current default row of this header. The default row is a + * special header row providing a user interface for sorting columns. + * + * @return the default row or null if no default row set + */ + public HeaderRow getDefaultRow() { + return defaultRow; + } + + @Override + protected GridStaticSectionState getSectionState() { + return headerState; + } + + @Override + protected HeaderRow createRow() { + return new HeaderRow(this); + } + + @Override + public HeaderRow removeRow(int rowIndex) { + HeaderRow row = super.removeRow(rowIndex); + if (row == defaultRow) { + // Default Header Row was just removed. + setDefaultRow(null); + } + return row; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridStaticSection.java b/server/src/com/vaadin/ui/components/grid/GridStaticSection.java new file mode 100644 index 0000000000..eb098d0d4e --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridStaticSection.java @@ -0,0 +1,425 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container.Indexed; +import com.vaadin.shared.ui.grid.GridStaticCellType; +import com.vaadin.shared.ui.grid.GridStaticSectionState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState; +import com.vaadin.ui.Component; + +/** + * Abstract base class for Grid header and footer sections. + * + * @since + * @author Vaadin Ltd + * @param <ROWTYPE> + * the type of the rows in the section + */ +abstract class GridStaticSection<ROWTYPE extends GridStaticSection.StaticRow<?>> + implements Serializable { + + /** + * Abstract base class for Grid header and footer rows. + * + * @param <CELLTYPE> + * the type of the cells in the row + */ + abstract static class StaticRow<CELLTYPE extends StaticCell> implements + Serializable { + + private RowState rowState = new RowState(); + protected GridStaticSection<?> section; + private Map<Object, CELLTYPE> cells = new LinkedHashMap<Object, CELLTYPE>(); + private Collection<List<CELLTYPE>> cellGroups = new HashSet<List<CELLTYPE>>(); + + protected StaticRow(GridStaticSection<?> section) { + this.section = section; + } + + protected void addCell(Object propertyId) { + CELLTYPE cell = createCell(); + cells.put(propertyId, cell); + rowState.cells.add(cell.getCellState()); + } + + /** + * Creates and returns a new instance of the cell type. + * + * @return the created cell + */ + protected abstract CELLTYPE createCell(); + + protected RowState getRowState() { + return rowState; + } + + /** + * Returns the cell at the given position in this row. + * + * @param propertyId + * the itemId of column + * @return the cell on given column + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public CELLTYPE getCell(Object propertyId) { + return cells.get(propertyId); + } + + /** + * Merges cells in a row + * + * @param cells + * The cells to be merged + * @return The first cell of the merged cells + */ + protected CELLTYPE join(List<CELLTYPE> cells) { + assert cells.size() > 1 : "You cannot merge less than 2 cells together"; + + // Ensure no cell is already grouped + for (CELLTYPE cell : cells) { + if (getCellGroupForCell(cell) != null) { + throw new IllegalStateException("Cell " + cell.getText() + + " is already grouped."); + } + } + + // Ensure continuous range + Iterator<CELLTYPE> cellIterator = this.cells.values().iterator(); + CELLTYPE current = null; + int firstIndex = 0; + + while (cellIterator.hasNext()) { + current = cellIterator.next(); + if (current == cells.get(0)) { + break; + } + firstIndex++; + } + + for (int i = 1; i < cells.size(); ++i) { + current = cellIterator.next(); + + if (current != cells.get(i)) { + throw new IllegalStateException( + "Cell range must be a continous range"); + } + } + + // Create a new group + final ArrayList<CELLTYPE> cellGroup = new ArrayList<CELLTYPE>(cells); + cellGroups.add(cellGroup); + + // Add group to state + List<Integer> stateGroup = new ArrayList<Integer>(); + for (int i = 0; i < cells.size(); ++i) { + stateGroup.add(firstIndex + i); + } + rowState.cellGroups.add(stateGroup); + section.markAsDirty(); + + // Returns first cell of group + return cells.get(0); + } + + /** + * Merges columns cells in a row + * + * @param properties + * The column properties which header should be merged + * @return The remaining visible cell after the merge + */ + public CELLTYPE join(Object... properties) { + List<CELLTYPE> cells = new ArrayList<CELLTYPE>(); + for (int i = 0; i < properties.length; ++i) { + cells.add(getCell(properties[i])); + } + + return join(cells); + } + + /** + * Merges columns cells in a row + * + * @param cells + * The cells to merge. Must be from the same row. + * @return The remaining visible cell after the merge + */ + public CELLTYPE join(CELLTYPE... cells) { + return join(Arrays.asList(cells)); + } + + private List<CELLTYPE> getCellGroupForCell(CELLTYPE cell) { + for (List<CELLTYPE> group : cellGroups) { + if (group.contains(cell)) { + return group; + } + } + return null; + } + } + + /** + * A header or footer cell. Has a simple textual caption. + */ + abstract static class StaticCell implements Serializable { + + private CellState cellState = new CellState(); + private StaticRow<?> row; + + protected StaticCell(StaticRow<?> row) { + this.row = row; + } + + /** + * Gets the row where this cell is. + * + * @return row for this cell + */ + public StaticRow<?> getRow() { + return row; + } + + protected CellState getCellState() { + return cellState; + } + + /** + * Sets the text displayed in this cell. + * + * @param text + * a plain text caption + */ + public void setText(String text) { + cellState.text = text; + cellState.type = GridStaticCellType.TEXT; + row.section.markAsDirty(); + } + + /** + * Returns the text displayed in this cell. + * + * @return the plain text caption + */ + public String getText() { + if (cellState.type != GridStaticCellType.TEXT) { + throw new IllegalStateException( + "Cannot fetch Text from a cell with type " + + cellState.type); + } + return cellState.text; + } + + /** + * Returns the HTML content displayed in this cell. + * + * @return the html + * + */ + public String getHtml() { + if (cellState.type != GridStaticCellType.HTML) { + throw new IllegalStateException( + "Cannot fetch HTML from a cell with type " + + cellState.type); + } + return cellState.html; + } + + /** + * Sets the HTML content displayed in this cell. + * + * @param html + * the html to set + */ + public void setHtml(String html) { + cellState.html = html; + cellState.type = GridStaticCellType.HTML; + row.section.markAsDirty(); + } + + /** + * Returns the component displayed in this cell. + * + * @return the component + */ + public Component getComponent() { + if (cellState.type != GridStaticCellType.WIDGET) { + throw new IllegalStateException( + "Cannot fetch Component from a cell with type " + + cellState.type); + } + return (Component) cellState.connector; + } + + /** + * Sets the component displayed in this cell. + * + * @param component + * the component to set + */ + public void setComponent(Component component) { + component.setParent(row.section.grid); + cellState.connector = component; + cellState.type = GridStaticCellType.WIDGET; + row.section.markAsDirty(); + } + } + + protected Grid grid; + protected List<ROWTYPE> rows = new ArrayList<ROWTYPE>(); + + /** + * Sets the visibility of the whole section. + * + * @param visible + * true to show this section, false to hide + */ + public void setVisible(boolean visible) { + if (getSectionState().visible != visible) { + getSectionState().visible = visible; + markAsDirty(); + } + } + + /** + * Returns the visibility of this section. + * + * @return true if visible, false otherwise. + */ + public boolean isVisible() { + return getSectionState().visible; + } + + /** + * Removes the row at the given position. + * + * @param index + * the position of the row + * + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public ROWTYPE removeRow(int rowIndex) { + ROWTYPE row = rows.remove(rowIndex); + getSectionState().rows.remove(rowIndex); + + markAsDirty(); + return row; + } + + /** + * Removes the given row from the section. + * + * @param row + * the row to be removed + * + * @throws IllegalArgumentException + * if the row does not exist in this section + */ + public void removeRow(ROWTYPE row) { + try { + removeRow(rows.indexOf(row)); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException( + "Section does not contain the given row"); + } + } + + /** + * Gets row at given index. + * + * @param rowIndex + * 0 based index for row. Counted from top to bottom + * @return row at given index + */ + public ROWTYPE getRow(int rowIndex) { + return rows.get(rowIndex); + } + + /** + * Adds a new row at the top of this section. + * + * @return the new row + */ + public ROWTYPE prependRow() { + return addRowAt(0); + } + + /** + * Adds a new row at the bottom of this section. + * + * @return the new row + */ + public ROWTYPE appendRow() { + return addRowAt(rows.size()); + } + + /** + * Inserts a new row at the given position. + * + * @param index + * the position at which to insert the row + * @return the new row + * + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public ROWTYPE addRowAt(int index) { + ROWTYPE row = createRow(); + rows.add(index, row); + getSectionState().rows.add(index, row.getRowState()); + + Indexed dataSource = grid.getContainerDatasource(); + for (Object id : dataSource.getContainerPropertyIds()) { + row.addCell(id); + } + + markAsDirty(); + return row; + } + + /** + * Gets the amount of rows in this section. + * + * @return row count + */ + public int getRowCount() { + return rows.size(); + } + + protected abstract GridStaticSectionState getSectionState(); + + protected abstract ROWTYPE createRow(); + + /** + * Informs the grid that state has changed and it should be redrawn. + */ + protected void markAsDirty() { + grid.markAsDirty(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/Renderer.java b/server/src/com/vaadin/ui/components/grid/Renderer.java new file mode 100644 index 0000000000..b9074fb9f7 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/Renderer.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.Extension; + +/** + * A ClientConnector for controlling client-side + * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. Renderers + * currently extend the Extension interface, but this fact should be regarded as + * an implementation detail and subject to change in a future major or minor + * Vaadin revision. + * + * @param <T> + * the type this renderer knows how to present + * + * @since + * @author Vaadin Ltd + */ +public interface Renderer<T> extends Extension { + + /** + * Returns the class literal corresponding to the presentation type T. + * + * @return the class literal of T + */ + Class<T> getPresentationType(); + + /** + * Encodes the given value into a form that can be transferred to the + * client. The type of the returned value must be one of the types that are + * accepted by <a href= + * "http://www.json.org/javadoc/org/json/JSONObject.html#put%28java.lang.String,%20java.lang.Object%29" + * >{@code org.json.JSONObject#put(String, Object)}</a>. + * + * @param value + * the value to encode + * @return an encoded form of the given value + */ + Object encode(T value); + + /** + * This method is inherited from Extension but should never be called + * directly with a Renderer. + */ + @Override + @Deprecated + void remove(); + + /** + * This method is inherited from Extension but should never be called + * directly with a Renderer. + */ + @Override + @Deprecated + void setParent(ClientConnector parent); +} diff --git a/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java b/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java new file mode 100644 index 0000000000..71afa10a9b --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java @@ -0,0 +1,57 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import java.util.List; + +import com.vaadin.ui.Component; +import com.vaadin.ui.components.grid.sort.SortOrder; + +/** + * Event fired by {@link Grid} when the sort order has changed. + * + * @see SortOrderChangeListener + * + * @since + * @author Vaadin Ltd + */ +public class SortOrderChangeEvent extends Component.Event { + + private final List<SortOrder> sortOrder; + + /** + * Creates a new sort order change event for a grid and a sort order list. + * + * @param grid + * the grid from which the event originates + * @param sortOrder + * the new sort order list + */ + public SortOrderChangeEvent(Grid grid, List<SortOrder> sortOrder) { + super(grid); + this.sortOrder = sortOrder; + } + + /** + * Gets the sort order list. + * + * @return the sort order list + */ + public List<SortOrder> getSortOrder() { + return sortOrder; + } + +} diff --git a/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java b/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java new file mode 100644 index 0000000000..82d7ba3108 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import java.io.Serializable; + +/** + * Listener for sort order change events from {@link Grid}. + * + * @since + * @author Vaadin Ltd + */ +public interface SortOrderChangeListener extends Serializable { + /** + * Called when the sort order has changed. + * + * @param event + * the sort order change event + */ + public void sortOrderChange(SortOrderChangeEvent event); +} diff --git a/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java new file mode 100644 index 0000000000..736b61d9e2 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java @@ -0,0 +1,152 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.renderers; + +import java.text.DateFormat; +import java.util.Date; +import java.util.Locale; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +/** + * A renderer for presenting date values. + * + * @since + * @author Vaadin Ltd + */ +public class DateRenderer extends AbstractRenderer<Date> { + private final Locale locale; + private final String formatString; + private final DateFormat dateFormat; + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the {@link Date#toString()} + * representation for the default locale. + */ + public DateRenderer() { + this(Locale.getDefault()); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the {@link Date#toString()} + * representation for the given locale. + * + * @param locale + * the locale in which to present dates + * @throws IllegalArgumentException + * if {@code locale} is {@code null} + */ + public DateRenderer(Locale locale) throws IllegalArgumentException { + this("%s", locale); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the given string format, as + * displayed in the default locale. + * + * @param formatString + * the format string with which to format the date + * @throws IllegalArgumentException + * if {@code formatString} is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public DateRenderer(String formatString) throws IllegalArgumentException { + this(formatString, Locale.getDefault()); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the given string format, as + * displayed in the given locale. + * + * @param formatString + * the format string to format the date with + * @param locale + * the locale to use + * @throws IllegalArgumentException + * if either argument is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public DateRenderer(String formatString, Locale locale) + throws IllegalArgumentException { + super(Date.class); + + if (formatString == null) { + throw new IllegalArgumentException("format string may not be null"); + } + + if (locale == null) { + throw new IllegalArgumentException("locale may not be null"); + } + + this.locale = locale; + this.formatString = formatString; + dateFormat = null; + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with he given date format. + * + * @param dateFormat + * the date format to use when rendering dates + * @throws IllegalArgumentException + * if {@code dateFormat} is {@code null} + */ + public DateRenderer(DateFormat dateFormat) throws IllegalArgumentException { + super(Date.class); + if (dateFormat == null) { + throw new IllegalArgumentException("date format may not be null"); + } + + locale = null; + formatString = null; + this.dateFormat = dateFormat; + } + + @Override + public String encode(Date value) { + if (dateFormat != null) { + return dateFormat.format(value); + } else { + return String.format(locale, formatString, value); + } + } + + @Override + public String toString() { + final String fieldInfo; + if (dateFormat != null) { + fieldInfo = "dateFormat: " + dateFormat.toString(); + } else { + fieldInfo = "locale: " + locale + ", formatString: " + formatString; + } + + return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java new file mode 100644 index 0000000000..6439608c20 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.renderers; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +/** + * A renderer for presenting HTML content. + * + * @since + * @author Vaadin Ltd + */ +public class HtmlRenderer extends AbstractRenderer<String> { + /** + * Creates a new HTML renderer. + */ + public HtmlRenderer() { + super(String.class); + } + + @Override + public String encode(String value) { + return value; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java new file mode 100644 index 0000000000..12fcfc890a --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java @@ -0,0 +1,159 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.renderers; + +import java.text.NumberFormat; +import java.util.Locale; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +/** + * A renderer for presenting number values. + * + * @since + * @author Vaadin Ltd + */ +public class NumberRenderer extends AbstractRenderer<Number> { + private final Locale locale; + private final NumberFormat numberFormat; + private final String formatString; + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the number's natural string + * representation in the default locale. + */ + public NumberRenderer() { + this(Locale.getDefault()); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render the number as defined with the given + * number format. + * + * @param numberFormat + * the number format with which to display numbers + * @throws IllegalArgumentException + * if {@code numberFormat} is {@code null} + */ + public NumberRenderer(NumberFormat numberFormat) + throws IllegalArgumentException { + super(Number.class); + + if (numberFormat == null) { + throw new IllegalArgumentException("Number format may not be null"); + } + + locale = null; + this.numberFormat = numberFormat; + formatString = null; + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the number's natural string + * representation in the given locale. + * + * @param locale + * the locale in which to display numbers + * @throws IllegalArgumentException + * if {@code locale} is {@code null} + */ + public NumberRenderer(Locale locale) throws IllegalArgumentException { + this("%s", locale); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the given format string in the + * default locale. + * + * @param formatString + * the format string with which to format the number + * @throws IllegalArgumentException + * if {@code formatString} is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public NumberRenderer(String formatString) throws IllegalArgumentException { + this(formatString, Locale.getDefault()); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the given format string in the + * given locale. + * + * @param formatString + * the format string with which to format the number + * @param locale + * the locale in which to present numbers + * @throws IllegalArgumentException + * if either argument is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public NumberRenderer(String formatString, Locale locale) { + super(Number.class); + + if (formatString == null) { + throw new IllegalArgumentException("Format string may not be null"); + } + + if (locale == null) { + throw new IllegalArgumentException("Locale may not be null"); + } + + this.locale = locale; + numberFormat = null; + this.formatString = formatString; + } + + @Override + public String encode(Number value) { + if (formatString != null && locale != null) { + return String.format(locale, formatString, value); + } else if (numberFormat != null) { + return numberFormat.format(value); + } else { + throw new IllegalStateException(String.format("Internal bug: " + + "%s is in an illegal state: " + + "[locale: %s, numberFormat: %s, formatString: %s]", + getClass().getSimpleName(), locale, numberFormat, + formatString)); + } + } + + @Override + public String toString() { + final String fieldInfo; + if (numberFormat != null) { + fieldInfo = "numberFormat: " + numberFormat.toString(); + } else { + fieldInfo = "locale: " + locale + ", formatString: " + formatString; + } + + return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java new file mode 100644 index 0000000000..61348a9e49 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.renderers; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +/** + * A renderer for presenting simple plain-text string values. + * + * @since + * @author Vaadin Ltd + */ +public class TextRenderer extends AbstractRenderer<String> { + + /** + * Creates a new text renderer + */ + public TextRenderer() { + super(String.class); + } + + @Override + public Object encode(String value) { + return value; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java new file mode 100644 index 0000000000..e153b8a4e4 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; + +import com.vaadin.ui.components.grid.Grid; + +/** + * A base class for SelectionModels that contains some of the logic that is + * reusable. + * + * @since + * @author Vaadin Ltd + */ +public abstract class AbstractSelectionModel implements SelectionModel { + protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>(); + protected Grid grid = null; + + @Override + public boolean isSelected(final Object itemId) { + return selection.contains(itemId); + } + + @Override + public Collection<Object> getSelectedRows() { + return new ArrayList<Object>(selection); + } + + @Override + public void setGrid(final Grid grid) { + this.grid = grid; + } + + /** + * Fires a {@link SelectionChangeEvent} to all the + * {@link SelectionChangeListener SelectionChangeListeners} currently added + * to the Grid in which this SelectionModel is. + * <p> + * Note that this is only a helper method, and routes the call all the way + * to Grid. A {@link SelectionModel} is not a + * {@link SelectionChangeNotifier} + * + * @param oldSelection + * the complete {@link Collection} of the itemIds that were + * selected <em>before</em> this event happened + * @param newSelection + * the complete {@link Collection} of the itemIds that are + * selected <em>after</em> this event happened + */ + protected void fireSelectionChangeEvent( + final Collection<Object> oldSelection, + final Collection<Object> newSelection) { + grid.fireSelectionChangeEvent(oldSelection, newSelection); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java new file mode 100644 index 0000000000..602e5ca169 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java @@ -0,0 +1,138 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import com.vaadin.data.Container.Indexed; + +/** + * A default implementation of a {@link SelectionModel.Multi} + * + * @since + * @author Vaadin Ltd + */ +public class MultiSelectionModel extends AbstractSelectionModel implements + SelectionModel.Multi { + + @Override + public boolean select(final Object... itemIds) + throws IllegalArgumentException { + if (itemIds != null) { + // select will fire the event + return select(Arrays.asList(itemIds)); + } else { + throw new IllegalArgumentException( + "Vararg array of itemIds may not be null"); + } + } + + @Override + public boolean select(final Collection<?> itemIds) + throws IllegalArgumentException { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds may not be null"); + } + + final boolean hasSomeDifferingElements = !selection + .containsAll(itemIds); + if (hasSomeDifferingElements) { + final HashSet<Object> oldSelection = new HashSet<Object>(selection); + selection.addAll(itemIds); + fireSelectionChangeEvent(oldSelection, selection); + } + return hasSomeDifferingElements; + } + + @Override + public boolean deselect(final Object... itemIds) + throws IllegalArgumentException { + if (itemIds != null) { + // deselect will fire the event + return deselect(Arrays.asList(itemIds)); + } else { + throw new IllegalArgumentException( + "Vararg array of itemIds may not be null"); + } + } + + @Override + public boolean deselect(final Collection<?> itemIds) + throws IllegalArgumentException { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds may not be null"); + } + + final boolean hasCommonElements = !Collections.disjoint(itemIds, + selection); + if (hasCommonElements) { + final HashSet<Object> oldSelection = new HashSet<Object>(selection); + selection.removeAll(itemIds); + fireSelectionChangeEvent(oldSelection, selection); + } + return hasCommonElements; + } + + @Override + public boolean selectAll() { + // select will fire the event + final Indexed container = grid.getContainerDatasource(); + if (container != null) { + return select(container.getItemIds()); + } else if (selection.isEmpty()) { + return false; + } else { + /* + * this should never happen (no container but has a selection), but + * I guess the only theoretically correct course of action... + */ + return deselectAll(); + } + } + + @Override + public boolean deselectAll() { + // deselect will fire the event + return deselect(getSelectedRows()); + } + + /** + * {@inheritDoc} + * <p> + * The returned Collection is in <strong>order of selection</strong> – + * the item that was first selected will be first in the collection, and so + * on. Should an item have been selected twice without being deselected in + * between, it will have remained in its original position. + */ + @Override + public Collection<Object> getSelectedRows() { + // overridden only for JavaDoc + return super.getSelectedRows(); + } + + /** + * Resets the selection model. + * <p> + * Equivalent to calling {@link #deselectAll()} + */ + @Override + public void reset() { + deselectAll(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java new file mode 100644 index 0000000000..89c31398ea --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.ui.components.grid.Grid; + +/** + * A default implementation for a {@link SelectionModel.None} + * + * @since + * @author Vaadin Ltd + */ +public class NoSelectionModel implements SelectionModel.None { + @Override + public void setGrid(final Grid grid) { + // NOOP, not needed for anything + } + + @Override + public boolean isSelected(final Object itemId) { + return false; + } + + @Override + public Collection<Object> getSelectedRows() { + return Collections.emptyList(); + } + + /** + * Semantically resets the selection model. + * <p> + * Effectively a no-op. + */ + @Override + public void reset() { + // NOOP + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java new file mode 100644 index 0000000000..af6a37dfde --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.Collection; +import java.util.EventObject; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.google.gwt.thirdparty.guava.common.collect.Sets; +import com.vaadin.ui.components.grid.Grid; + +/** + * An event that specifies what in a selection has changed, and where the + * selection took place. + * + * @since + * @author Vaadin Ltd + */ +public class SelectionChangeEvent extends EventObject { + + private LinkedHashSet<Object> oldSelection; + private LinkedHashSet<Object> newSelection; + + public SelectionChangeEvent(Grid source, Collection<Object> oldSelection, + Collection<Object> newSelection) { + super(source); + this.oldSelection = new LinkedHashSet<Object>(oldSelection); + this.newSelection = new LinkedHashSet<Object>(newSelection); + } + + /** + * A {@link Collection} of all the itemIds that became selected. + * <p> + * <em>Note:</em> this excludes all itemIds that might have been previously + * selected. + * + * @return a Collection of the itemIds that became selected + */ + public Set<Object> getAdded() { + return Sets.difference(newSelection, oldSelection); + } + + /** + * A {@link Collection} of all the itemIds that became deselected. + * <p> + * <em>Note:</em> this excludes all itemIds that might have been previously + * deselected. + * + * @return a Collection of the itemIds that became deselected + */ + public Set<Object> getRemoved() { + return Sets.difference(oldSelection, newSelection); + } + + @Override + public Grid getSource() { + return (Grid) super.getSource(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java new file mode 100644 index 0000000000..0d10e8c74d --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.io.Serializable; + +/** + * The listener interface for receiving {@link SelectionChangeEvent + * SelectionChangeEvents}. + * + * @since + * @author Vaadin Ltd + */ +public interface SelectionChangeListener extends Serializable { + /** + * Notifies the listener that the selection state has changed. + * + * @param event + * the selection change event + */ + void selectionChange(SelectionChangeEvent event); +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java new file mode 100644 index 0000000000..40cef965dd --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.io.Serializable; + +/** + * The interface for adding and removing listeners for + * {@link SelectionChangeEvent SelectionChangeEvents}. + * + * @since + * @author Vaadin Ltd + */ +public interface SelectionChangeNotifier extends Serializable { + /** + * Registers a new selection change listener + * + * @param listener + * the listener to register + */ + void addSelectionChangeListener(SelectionChangeListener listener); + + /** + * Removes a previously registered selection change listener + * + * @param listener + * the listener to remove + */ + void removeSelectionChangeListener(SelectionChangeListener listener); +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java new file mode 100644 index 0000000000..60bb130ab1 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java @@ -0,0 +1,234 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.io.Serializable; +import java.util.Collection; + +import com.vaadin.ui.components.grid.Grid; + +/** + * The server-side interface that controls Grid's selection state. + * + * @since + * @author Vaadin Ltd + */ +public interface SelectionModel extends Serializable { + /** + * Checks whether an item is selected or not. + * + * @param itemId + * the item id to check for + * @return <code>true</code> iff the item is selected + */ + boolean isSelected(Object itemId); + + /** + * Returns a collection of all the currently selected itemIds. + * + * @return a collection of all the currently selected itemIds + */ + Collection<Object> getSelectedRows(); + + /** + * Injects the current {@link Grid} instance into the SelectionModel. + * <p> + * <em>Note:</em> This method should not be called manually. + * + * @param grid + * the Grid in which the SelectionModel currently is, or + * <code>null</code> when a selection model is being detached + * from a Grid. + */ + void setGrid(Grid grid); + + /** + * Resets the SelectiomModel to an initial state. + * <p> + * Most often this means that the selection state is cleared, but + * implementations are free to interpret the "initial state" as they wish. + * Some, for example, may want to keep the first selected item as selected. + */ + void reset(); + + /** + * A SelectionModel that supports multiple selections to be made. + * <p> + * This interface has a contract of having the same behavior, no matter how + * the selection model is interacted with. In other words, if something is + * forbidden to do in e.g. the user interface, it must also be forbidden to + * do in the server-side and client-side APIs. + */ + public interface Multi extends SelectionModel { + + /** + * Marks items as selected. + * <p> + * This method does not clear any previous selection state, only adds to + * it. + * + * @param itemIds + * the itemId(s) to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if all the given itemIds already were + * selected + * @throws IllegalArgumentException + * if the <code>itemIds</code> varargs array is + * <code>null</code> + * @see #deselect(Object...) + */ + boolean select(Object... itemIds) throws IllegalArgumentException; + + /** + * Marks items as selected. + * <p> + * This method does not clear any previous selection state, only adds to + * it. + * + * @param itemIds + * the itemIds to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if all the given itemIds already were + * selected + * @throws IllegalArgumentException + * if <code>itemIds</code> is <code>null</code> + * @see #deselect(Collection) + */ + boolean select(Collection<?> itemIds) throws IllegalArgumentException; + + /** + * Marks items as deselected. + * + * @param itemIds + * the itemId(s) to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if none the given itemIds were selected + * previously + * @throws IllegalArgumentException + * if the <code>itemIds</code> varargs array is + * <code>null</code> + * @see #select(Object...) + */ + boolean deselect(Object... itemIds) throws IllegalArgumentException; + + /** + * Marks items as deselected. + * + * @param itemIds + * the itemId(s) to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if none the given itemIds were selected + * previously + * @throws IllegalArgumentException + * if <code>itemIds</code> is <code>null</code> + * @see #select(Collection) + */ + boolean deselect(Collection<?> itemIds) throws IllegalArgumentException; + + /** + * Marks all the items in the current Container as selected + * + * @return <code>true</code> iff some items were previously not selected + * @see #deselectAll() + */ + boolean selectAll(); + + /** + * Marks all the items in the current Container as deselected + * + * @return <code>true</code> iff some items were previously selected + * @see #selectAll() + */ + boolean deselectAll(); + } + + /** + * A SelectionModel that supports for only single rows to be selected at a + * time. + * <p> + * This interface has a contract of having the same behavior, no matter how + * the selection model is interacted with. In other words, if something is + * forbidden to do in e.g. the user interface, it must also be forbidden to + * do in the server-side and client-side APIs. + */ + public interface Single extends SelectionModel { + /** + * Marks an item as selected. + * + * @param itemIds + * the itemId to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalStateException + * if the selection was illegal. One such reason might be + * that the implementation already had an item selected, and + * that needs to be explicitly deselected before + * re-selecting something + * @see #deselect(Object) + */ + boolean select(Object itemId) throws IllegalStateException; + + /** + * Marks an item as deselected. + * + * @param itemId + * the itemId to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalStateException + * if the deselection was illegal. One such reason might be + * that the implementation enforces that an item is always + * selected + * @see #select(Object) + */ + boolean deselect(Object itemId) throws IllegalStateException; + + /** + * Gets the item id of the currently selected item. + * + * @return the item id of the currently selected item, or + * <code>null</code> if nothing is selected + */ + Object getSelectedRow(); + } + + /** + * A SelectionModel that does not allow for rows to be selected. + * <p> + * This interface has a contract of having the same behavior, no matter how + * the selection model is interacted with. In other words, if the developer + * is unable to select something programmatically, it is not allowed for the + * end-user to select anything, either. + */ + public interface None extends SelectionModel { + + /** + * {@inheritDoc} + * + * @return always <code>false</code>. + */ + @Override + public boolean isSelected(Object itemId); + + /** + * {@inheritDoc} + * + * @return always an empty collection. + */ + @Override + public Collection<Object> getSelectedRows(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java new file mode 100644 index 0000000000..0f6e8a296d --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.Collection; +import java.util.Collections; + +/** + * A default implementation of a {@link SelectionModel.Single} + * + * @since + * @author Vaadin Ltd + */ +public class SingleSelectionModel extends AbstractSelectionModel implements + SelectionModel.Single { + @Override + public boolean select(final Object itemId) { + final Object selectedRow = getSelectedRow(); + final boolean modified = selection.add(itemId); + if (modified) { + final Collection<Object> deselected; + if (selectedRow != null) { + deselectInternal(selectedRow, false); + deselected = Collections.singleton(selectedRow); + } else { + deselected = Collections.emptySet(); + } + + fireSelectionChangeEvent(deselected, selection); + } + + return modified; + } + + @Override + public boolean deselect(final Object itemId) { + return deselectInternal(itemId, true); + } + + private boolean deselectInternal(final Object itemId, + boolean fireEventIfNeeded) { + final boolean modified = selection.remove(itemId); + if (fireEventIfNeeded && modified) { + fireSelectionChangeEvent(Collections.singleton(itemId), + Collections.emptySet()); + } + return modified; + } + + @Override + public Object getSelectedRow() { + if (selection.isEmpty()) { + return null; + } else { + return selection.iterator().next(); + } + } + + /** + * Resets the selection state. + * <p> + * If an item is selected, it will become deselected. + */ + @Override + public void reset() { + deselect(getSelectedRow()); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/sort/Sort.java b/server/src/com/vaadin/ui/components/grid/sort/Sort.java new file mode 100644 index 0000000000..54831378b6 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/sort/Sort.java @@ -0,0 +1,153 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.sort; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Fluid Sort API. Provides a convenient, human-readable way of specifying + * multi-column sort order. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class Sort implements Serializable { + + private final Sort previous; + private final SortOrder order; + + /** + * Initial constructor, called by the static by() methods. + * + * @param propertyId + * a property ID, corresponding to a property in the data source + * @param direction + * a sort direction value + */ + private Sort(Object propertyId, SortDirection direction) { + previous = null; + order = new SortOrder(propertyId, direction); + } + + /** + * Chaining constructor, called by the non-static then() methods. This + * constructor links to the previous Sort object. + * + * @param previous + * the sort marker that comes before this one + * @param propertyId + * a property ID, corresponding to a property in the data source + * @param direction + * a sort direction value + */ + private Sort(Sort previous, Object propertyId, SortDirection direction) { + this.previous = previous; + order = new SortOrder(propertyId, direction); + + Sort s = previous; + while (s != null) { + if (s.order.getPropertyId() == propertyId) { + throw new IllegalStateException( + "Can not sort along the same property (" + propertyId + + ") twice!"); + } + s = s.previous; + } + + } + + /** + * Start building a Sort order by sorting a provided column in ascending + * order. + * + * @param propertyId + * a property id, corresponding to a data source property + * @return a sort object + */ + public static Sort by(Object propertyId) { + return by(propertyId, SortDirection.ASCENDING); + } + + /** + * Start building a Sort order by sorting a provided column. + * + * @param propertyId + * a property id, corresponding to a data source property + * @param direction + * a sort direction value + * @return a sort object + */ + public static Sort by(Object propertyId, SortDirection direction) { + return new Sort(propertyId, direction); + } + + /** + * Continue building a Sort order. The provided property is sorted in + * ascending order if the previously added properties have been evaluated as + * equals. + * + * @param propertyId + * a property id, corresponding to a data source property + * @return a sort object + */ + public Sort then(Object propertyId) { + return then(propertyId, SortDirection.ASCENDING); + } + + /** + * Continue building a Sort order. The provided property is sorted in + * specified order if the previously added properties have been evaluated as + * equals. + * + * @param propertyId + * a property id, corresponding to a data source property + * @param direction + * a sort direction value + * @return a sort object + */ + public Sort then(Object propertyId, SortDirection direction) { + return new Sort(this, propertyId, direction); + } + + /** + * Build a sort order list, ready to be passed to Grid + * + * @return a sort order list. + */ + public List<SortOrder> build() { + + int count = 1; + Sort s = this; + while (s.previous != null) { + s = s.previous; + ++count; + } + + List<SortOrder> order = new ArrayList<SortOrder>(count); + + s = this; + do { + order.add(0, s.order); + s = s.previous; + } while (s != null); + + return order; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java b/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java new file mode 100644 index 0000000000..a76148fe0c --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.sort; + +import java.io.Serializable; + +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Sort order descriptor. Links together a {@link SortDirection} value and a + * Vaadin container property ID. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class SortOrder implements Serializable { + + private final Object propertyId; + private final SortDirection direction; + + /** + * Create a SortOrder object. Both arguments must be non-null. + * + * @param propertyId + * id of the data source property to sort by + * @param direction + * value indicating whether the property id should be sorted in + * ascending or descending order + */ + public SortOrder(Object propertyId, SortDirection direction) { + if (propertyId == null) { + throw new IllegalArgumentException("Property ID can not be null!"); + } + if (direction == null) { + throw new IllegalArgumentException( + "Direction value can not be null!"); + } + this.propertyId = propertyId; + this.direction = direction; + } + + /** + * Returns the property ID. + * + * @return a property ID + */ + public Object getPropertyId() { + return propertyId; + } + + /** + * Returns the {@link SortDirection} value. + * + * @return a sort direction value + */ + public SortDirection getDirection() { + return direction; + } + + @Override + public String toString() { + return propertyId + " " + direction; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + direction.hashCode(); + result = prime * result + propertyId.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (getClass() != obj.getClass()) { + return false; + } + + SortOrder other = (SortOrder) obj; + if (direction != other.direction) { + return false; + } else if (!propertyId.equals(other.propertyId)) { + return false; + } + return true; + } + +} |