import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.ui.SimpleManagedLayout;
import com.vaadin.client.widget.grid.CellReference;
import com.vaadin.client.widget.grid.CellStyleGenerator;
+import com.vaadin.client.widget.grid.DetailsGenerator;
import com.vaadin.client.widget.grid.EditorHandler;
import com.vaadin.client.widget.grid.RowReference;
import com.vaadin.client.widget.grid.RowStyleGenerator;
*/
@Connect(com.vaadin.ui.Grid.class)
public class GridConnector extends AbstractHasComponentsConnector implements
- SimpleManagedLayout {
+ SimpleManagedLayout, RpcDataSourceConnector.DetailsListener {
private static final class CustomCellStyleGenerator implements
CellStyleGenerator<JsonObject> {
}
}
+ private class CustomDetailsGenerator implements DetailsGenerator {
+ @Override
+ public Widget getDetails(int rowIndex) {
+ // TODO
+ return new Label("[todo]");
+ }
+ }
+
/**
* Maps a generated column id to a grid column instance
*/
});
getWidget().setEditorHandler(new CustomEditorHandler());
+
+ getWidget().setDetailsGenerator(new CustomDetailsGenerator());
+
getLayoutManager().registerDependency(this, getWidget().getElement());
+
layout();
}
public void layout() {
getWidget().onResize();
}
+
+ @Override
+ public void reapplyDetailsVisibility(int rowIndex, JsonObject row) {
+ if (row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
+ && row.getBoolean(GridState.JSONKEY_DETAILS_VISIBLE)) {
+ getWidget().setDetailsVisible(rowIndex, true);
+ } else {
+ getWidget().setDetailsVisible(rowIndex, false);
+ }
+ }
}
package com.vaadin.client.connectors;
import java.util.ArrayList;
+import java.util.List;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.data.AbstractRemoteDataSource;
@Connect(com.vaadin.data.RpcDataProviderExtension.class)
public class RpcDataSourceConnector extends AbstractExtensionConnector {
+ /**
+ * A callback interface to let {@link GridConnector} know that detail
+ * visibilities might have changed.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+ interface DetailsListener {
+
+ /**
+ * A request to verify (and correct) the visibility for a row, given
+ * updated metadata.
+ *
+ * @param rowIndex
+ * the index of the row that should be checked
+ * @param row
+ * the row object to check visibility for
+ * @see GridState#JSONKEY_DETAILS_VISIBLE
+ */
+ void reapplyDetailsVisibility(int rowIndex, JsonObject row);
+ }
+
public class RpcDataSource extends AbstractRemoteDataSource<JsonObject> {
protected RpcDataSource() {
rows.add(rowObject);
}
- dataSource.setRowData(firstRow, rows);
+ RpcDataSource.this.setRowData(firstRow, rows);
}
@Override
public void removeRowData(int firstRow, int count) {
- dataSource.removeRowData(firstRow, count);
+ RpcDataSource.this.removeRowData(firstRow, count);
}
@Override
public void insertRowData(int firstRow, int count) {
- dataSource.insertRowData(firstRow, count);
+ RpcDataSource.this.insertRowData(firstRow, count);
}
@Override
public void resetDataAndSize(int size) {
- dataSource.resetDataAndSize(size);
+ RpcDataSource.this.resetDataAndSize(size);
}
});
}
private DataRequestRpc rpcProxy = getRpcProxy(DataRequestRpc.class);
+ private DetailsListener detailsListener;
@Override
protected void requestRows(int firstRowIndex, int numberOfRows,
if (!handle.isPinned()) {
rpcProxy.setPinned(key, false);
}
+ }
+
+ void setDetailsListener(DetailsListener detailsListener) {
+ this.detailsListener = detailsListener;
+ }
+ @Override
+ protected void setRowData(int firstRowIndex, List<JsonObject> rowData) {
+ super.setRowData(firstRowIndex, rowData);
+
+ /*
+ * Intercepting details information from the data source, rerouting
+ * them back to the GridConnector (as a details listener)
+ */
+ for (int i = 0; i < rowData.size(); i++) {
+ detailsListener.reapplyDetailsVisibility(firstRowIndex + i,
+ rowData.get(i));
+ }
}
}
@Override
protected void extend(ServerConnector target) {
- ((GridConnector) target).setDataSource(dataSource);
+ GridConnector gridConnector = (GridConnector) target;
+ dataSource.setDetailsListener(gridConnector);
+ gridConnector.setDataSource(dataSource);
}
}
*/
public interface DetailsGenerator {
+ /** A details generator that provides no details */
public static final DetailsGenerator NULL = new DetailsGenerator() {
@Override
public Widget getDetails(int rowIndex) {
* @since
* @param detailsGenerator
* the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
*/
public void setDetailsGenerator(DetailsGenerator detailsGenerator)
throws IllegalArgumentException {
+ if (detailsGenerator == null) {
+ throw new IllegalArgumentException(
+ "Details generator may not be null");
+ }
+
this.detailsGenerator = detailsGenerator;
// this will refresh all visible spacers
// private implementation
}
- void setActiveRange(Range newActiveRange) {
+ public void setActiveRange(Range newActiveRange) {
final Range[] removed = activeRange.partitionWith(newActiveRange);
final Range[] added = newActiveRange.partitionWith(activeRange);
return String.valueOf(rollingIndex++);
}
- String getKey(Object itemId) {
+ public String getKey(Object itemId) {
String key = itemIdToKey.get(itemId);
if (key == null) {
key = nextKey();
return itemIds;
}
+ /**
+ * Gets the row index for a given item.
+ *
+ * @since
+ * @param itemId
+ * the item id of the item for which to get the item
+ * @return the index of the item, or -1 if no such item could be found
+ */
+ @SuppressWarnings("boxing")
+ public int getIndex(Object itemId) {
+ Integer integer = indexToItemId.inverse().get(itemId);
+ return integer != null ? integer : -1;
+ }
+
/**
* Pin an item id to be cached indefinitely.
* <p>
return pinnedItemIds.contains(itemId);
}
- Object itemIdAtIndex(int index) {
+ private Object itemIdAtIndex(int index) {
return indexToItemId.get(Integer.valueOf(index));
}
}
/** Size possibly changed with a bare ItemSetChangeEvent */
private boolean bareItemSetTriggeredSizeChange = false;
+ /**
+ * This map represents all the details that are user-defined as visible.
+ * This does not reflect the status in the DOM.
+ */
+ private Set<Object> visibleDetails = new HashSet<Object>();
+
/**
* Creates a new data provider using the given container.
*
rowObject.put(GridState.JSONKEY_DATA, rowData);
rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId));
+ if (visibleDetails.contains(itemId)) {
+ rowObject.put(GridState.JSONKEY_DETAILS_VISIBLE, true);
+ }
+
rowReference.set(itemId);
CellStyleGenerator cellStyleGenerator = grid.getCellStyleGenerator();
return Logger.getLogger(RpcDataProviderExtension.class.getName());
}
+ /**
+ * Marks a row's details to be visible or hidden.
+ * <p>
+ * If that row is currently in the client side's cache, this information
+ * will be sent over to the client.
+ *
+ * @since
+ * @param itemId
+ * the id of the item of which to change the details visibility
+ * @param visible
+ * <code>true</code> to show the details, <code>false</code> to
+ * hide
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ final boolean modified;
+ if (visible) {
+ modified = visibleDetails.add(itemId);
+ } else {
+ modified = visibleDetails.remove(itemId);
+ }
+
+ int rowIndex = keyMapper.getIndex(itemId);
+ boolean modifiedRowIsActive = activeRowHandler.activeRange
+ .contains(rowIndex);
+ if (modified && modifiedRowIsActive) {
+ updateRowData(itemId);
+ }
+ }
+
+ /**
+ * Checks whether the details for a row is marked as visible.
+ *
+ * @since
+ * @param itemId
+ * the id of the item of which to check the visibility
+ * @return <code>true</code> iff the detials are visible for the item. This
+ * might return <code>true</code> even if the row is not currently
+ * visible in the DOM
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return visibleDetails.contains(itemId);
+ }
}
public class Grid extends AbstractComponent implements SelectionNotifier,
SortNotifier, SelectiveRenderer, ItemClickNotifier {
+ /**
+ * A callback interface for generating details for a particular row in Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+ public interface DetailsGenerator extends Serializable {
+
+ /** A details generator that provides no details */
+ public DetailsGenerator NULL = new DetailsGenerator() {
+ @Override
+ public Component getDetails(RowReference rowReference) {
+ return null;
+ }
+ };
+
+ /**
+ * This method is called for whenever a new details row needs to be
+ * generated.
+ *
+ * @param rowReference
+ * the reference for the row for which to generate details
+ * @return the details for the given row, or <code>null</code> to leave
+ * the details empty.
+ */
+ Component getDetails(RowReference rowReference);
+ }
+
/**
* Custom field group that allows finding property types before an item has
* been bound.
private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
+ private DetailsGenerator detailsGenerator = DetailsGenerator.NULL;
+
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionListener.class, "select", SelectionEvent.class);
public void recalculateColumnWidths() {
getRpcProxy(GridClientRpc.class).recalculateColumnWidths();
}
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @since
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+ if (detailsGenerator == null) {
+ throw new IllegalArgumentException(
+ "Details generator may not be null");
+ } else if (detailsGenerator == this.detailsGenerator) {
+ return;
+ }
+
+ this.detailsGenerator = detailsGenerator;
+
+ getLogger().warning("[[details]] update details on generator swap");
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @since
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailsGenerator;
+ }
+
+ /**
+ * Shows or hides the details for a specific item.
+ *
+ * @since
+ * @param itemId
+ * the id of the item for which to set details visibility
+ * @param visible
+ * <code>true</code> to show the details, or <code>false</code>
+ * to hide them
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ datasourceExtension.setDetailsVisible(itemId, visible);
+ }
+
+ /**
+ * Checks whether details are visible for the given item.
+ *
+ * @since
+ * @param itemId
+ * the id of the item for which to check details visibility
+ * @return <code>true</code> iff the details are visible
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return datasourceExtension.isDetailsVisible(itemId);
+ }
}
*/
public static final String JSONKEY_CELLSTYLES = "cs";
+ /**
+ * The key that tells whether details are visible for the row
+ *
+ * @see com.vaadin.ui.Grid#setDetailsGenerator(com.vaadin.ui.Grid.DetailsGenerator)
+ * @see com.vaadin.ui.Grid#setDetailsVisible(Object, boolean)
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int,
+ * elemental.json.JsonArray)
+ * */
+ public static final String JSONKEY_DETAILS_VISIBLE = "dv";
+
/**
* Columns in grid.
*/
addFilterActions();
+ createDetailsActions();
+
this.grid = grid;
return grid;
}
}, null);
}
+ private void createDetailsActions() {
+ createBooleanAction("firstItemId", "Details", false,
+ new Command<Grid, Boolean>() {
+ @Override
+ @SuppressWarnings("boxing")
+ public void execute(Grid g, Boolean visible, Object data) {
+ g.setDetailsVisible(g.getContainerDataSource()
+ .firstItemId(), visible);
+ }
+ });
+ }
+
@Override
protected Integer getTicketNumber() {
return 12829;
--- /dev/null
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.NoSuchElementException;
+
+import com.vaadin.testbench.annotations.RunLocally;
+import com.vaadin.testbench.parallel.Browser;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+@RunLocally(Browser.PHANTOMJS)
+public class GridDetailsServerTest extends GridBasicFeaturesTest {
+ private static final String[] FIRST_ITEM_DETAILS = new String[] {
+ "Component", "Details", "firstItemId" };
+
+ @Before
+ public void setUp() {
+ openTestURL();
+ }
+
+ @Test
+ public void openVisibleDetails() {
+ try {
+ getGridElement().getDetails(0);
+ fail("Expected NoSuchElementException");
+ } catch (NoSuchElementException ignore) {
+ // expected
+ }
+ selectMenuPath(FIRST_ITEM_DETAILS);
+ assertNotNull("details should've opened", getGridElement()
+ .getDetails(0));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void closeVisibleDetails() {
+ selectMenuPath(FIRST_ITEM_DETAILS);
+ selectMenuPath(FIRST_ITEM_DETAILS);
+ getGridElement().getDetails(0);
+ }
+
+ @Test
+ public void openDetailsOutsideOfActiveRange() {
+ getGridElement().scroll(10000);
+ selectMenuPath(FIRST_ITEM_DETAILS);
+ getGridElement().scroll(0);
+ assertNotNull("details should've been opened", getGridElement()
+ .getDetails(0));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void closeDetailsOutsideOfActiveRange() {
+ selectMenuPath(FIRST_ITEM_DETAILS);
+ getGridElement().scroll(10000);
+ selectMenuPath(FIRST_ITEM_DETAILS);
+ getGridElement().scroll(0);
+ getGridElement().getDetails(0);
+ }
+}