for the full
* license.
*/
package com.vaadin.v7.client.connectors;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Logger;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.DeferredWorker;
import com.vaadin.client.MouseEventDetailsBuilder;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
import com.vaadin.client.ui.AbstractComponentConnector;
import com.vaadin.client.ui.AbstractHasComponentsConnector;
import com.vaadin.client.ui.ConnectorFocusAndBlurHandler;
import com.vaadin.client.ui.SimpleManagedLayout;
import com.vaadin.client.ui.layout.ElementResizeEvent;
import com.vaadin.client.ui.layout.ElementResizeListener;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.ContentMode;
import com.vaadin.v7.client.connectors.RpcDataSourceConnector.DetailsListener;
import com.vaadin.v7.client.connectors.RpcDataSourceConnector.RpcDataSource;
import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedEvent;
import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedHandler;
import com.vaadin.v7.client.widget.grid.CellReference;
import com.vaadin.v7.client.widget.grid.CellStyleGenerator;
import com.vaadin.v7.client.widget.grid.EditorHandler;
import com.vaadin.v7.client.widget.grid.EventCellReference;
import com.vaadin.v7.client.widget.grid.HeightAwareDetailsGenerator;
import com.vaadin.v7.client.widget.grid.RowReference;
import com.vaadin.v7.client.widget.grid.RowStyleGenerator;
import com.vaadin.v7.client.widget.grid.events.BodyClickHandler;
import com.vaadin.v7.client.widget.grid.events.BodyDoubleClickHandler;
import com.vaadin.v7.client.widget.grid.events.ColumnReorderEvent;
import com.vaadin.v7.client.widget.grid.events.ColumnReorderHandler;
import com.vaadin.v7.client.widget.grid.events.ColumnResizeEvent;
import com.vaadin.v7.client.widget.grid.events.ColumnResizeHandler;
import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeEvent;
import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeHandler;
import com.vaadin.v7.client.widget.grid.events.GridClickEvent;
import com.vaadin.v7.client.widget.grid.events.GridDoubleClickEvent;
import com.vaadin.v7.client.widget.grid.sort.SortEvent;
import com.vaadin.v7.client.widget.grid.sort.SortHandler;
import com.vaadin.v7.client.widget.grid.sort.SortOrder;
import com.vaadin.v7.client.widgets.Grid;
import com.vaadin.v7.client.widgets.Grid.Column;
import com.vaadin.v7.client.widgets.Grid.FooterCell;
import com.vaadin.v7.client.widgets.Grid.FooterRow;
import com.vaadin.v7.client.widgets.Grid.HeaderCell;
import com.vaadin.v7.client.widgets.Grid.HeaderRow;
import com.vaadin.v7.shared.ui.grid.EditorClientRpc;
import com.vaadin.v7.shared.ui.grid.EditorServerRpc;
import com.vaadin.v7.shared.ui.grid.GridClientRpc;
import com.vaadin.v7.shared.ui.grid.GridColumnState;
import com.vaadin.v7.shared.ui.grid.GridConstants;
import com.vaadin.v7.shared.ui.grid.GridConstants.Section;
import com.vaadin.v7.shared.ui.grid.GridServerRpc;
import com.vaadin.v7.shared.ui.grid.GridState;
import com.vaadin.v7.shared.ui.grid.GridStaticSectionState;
import com.vaadin.v7.shared.ui.grid.GridStaticSectionState.CellState;
import com.vaadin.v7.shared.ui.grid.GridStaticSectionState.RowState;
import com.vaadin.v7.shared.ui.grid.ScrollDestination;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
/**
* Connects the client side {@link Grid} widget with the server side
* Grid
component.
*
* The Grid is typed to JSONObject. The structure of the JSONObject is described
* at {@link com.vaadin.shared.data.DataProviderRpc#setRowData(int, List)
* DataProviderRpc.setRowData(int, List)}.
*
* @since 7.4
* @author Vaadin Ltd
*/
@Connect(com.vaadin.v7.ui.Grid.class)
public class GridConnector extends AbstractHasComponentsConnector
implements SimpleManagedLayout, DeferredWorker {
private static final class CustomStyleGenerator implements
CellStyleGenerator, RowStyleGenerator {
@Override
public String getStyle(CellReference cellReference) {
JsonObject row = cellReference.getRow();
if (!row.hasKey(GridState.JSONKEY_CELLSTYLES)) {
return null;
}
Column, JsonObject> column = cellReference.getColumn();
if (!(column instanceof CustomGridColumn)) {
// Selection checkbox column
return null;
}
CustomGridColumn c = (CustomGridColumn) column;
JsonObject cellStylesObject = row
.getObject(GridState.JSONKEY_CELLSTYLES);
assert cellStylesObject != null;
if (cellStylesObject.hasKey(c.id)) {
return cellStylesObject.getString(c.id);
} else {
return null;
}
}
@Override
public String getStyle(RowReference rowReference) {
JsonObject row = rowReference.getRow();
if (row.hasKey(GridState.JSONKEY_ROWSTYLE)) {
return row.getString(GridState.JSONKEY_ROWSTYLE);
} else {
return null;
}
}
}
/**
* Custom implementation of the custom grid column using a JSONObject to
* represent the cell value and String as a column type.
*/
private class CustomGridColumn extends Grid.Column {
private final String id;
private AbstractGridRendererConnector rendererConnector;
private AbstractComponentConnector editorConnector;
private HandlerRegistration errorStateHandler;
public CustomGridColumn(String id,
AbstractGridRendererConnector rendererConnector) {
super(rendererConnector.getRenderer());
this.rendererConnector = rendererConnector;
this.id = id;
}
/**
* Creates and initializes a custom grid column with attributes of given state.
*
* @param state with attributes to initialize the column.
*/
@SuppressWarnings("unchecked")
private CustomGridColumn(GridColumnState state) {
this(state.id, (AbstractGridRendererConnector) state.rendererConnector);
this.hidingToggleCaption = state.hidingToggleCaption;
this.hidden = state.hidden;
this.hidable = state.hidable;
this.resizable = state.resizable;
this.sortable = state.sortable;
this.headerCaption = state.headerCaption == null ? "" : state.headerCaption;
this.widthUser = state.width;
this.minimumWidthPx = state.minWidth;
this.maximumWidthPx = state.maxWidth;
this.expandRatio = state.expandRatio;
this.editable = state.editable;
setEditorConnector((AbstractComponentConnector) state.editorConnector);
}
/**
* Sets a new renderer for this column object
*
* @param rendererConnector
* a renderer connector object
*/
public void setRenderer(
AbstractGridRendererConnector rendererConnector) {
setRenderer(rendererConnector.getRenderer());
this.rendererConnector = rendererConnector;
}
@Override
public Object getValue(final JsonObject obj) {
final JsonObject rowData = obj.getObject(GridState.JSONKEY_DATA);
if (rowData.hasKey(id)) {
final JsonValue columnValue = rowData.get(id);
return rendererConnector.decode(columnValue);
}
return null;
}
private AbstractComponentConnector getEditorConnector() {
return editorConnector;
}
private void setEditorConnector(
final AbstractComponentConnector editorConnector) {
this.editorConnector = editorConnector;
if (errorStateHandler != null) {
errorStateHandler.removeHandler();
errorStateHandler = null;
}
// Avoid nesting too deep
if (editorConnector == null) {
return;
}
errorStateHandler = editorConnector.addStateChangeHandler(
"errorMessage", new StateChangeHandler() {
@Override
public void onStateChanged(
StateChangeEvent stateChangeEvent) {
String error = editorConnector
.getState().errorMessage;
if (error == null) {
columnToErrorMessage
.remove(CustomGridColumn.this);
} else {
// The error message is formatted as HTML;
// therefore, we use this hack to make the
// string human-readable.
Element e = DOM.createElement("div");
e.setInnerHTML(editorConnector
.getState().errorMessage);
error = getHeaderCaption() + ": "
+ e.getInnerText();
columnToErrorMessage.put(CustomGridColumn.this,
error);
}
// Handle Editor RPC before updating error status
Scheduler.get()
.scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
updateErrorColumns();
}
});
}
public void updateErrorColumns() {
getWidget().getEditor().setEditorError(
getColumnErrors(),
columnToErrorMessage.keySet());
}
});
}
}
/*
* An editor handler using Vaadin RPC to manage the editor state.
*/
private class CustomEditorHandler implements EditorHandler {
private EditorServerRpc rpc = getRpcProxy(EditorServerRpc.class);
private EditorRequest currentRequest = null;
private boolean serverInitiated = false;
public CustomEditorHandler() {
registerRpc(EditorClientRpc.class, new EditorClientRpc() {
@Override
public void bind(final int rowIndex) {
// call this deferred to avoid issues with editing on init
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
if (!isReadOnly()) {
GridConnector.this.getWidget()
.editRow(rowIndex);
}
}
});
}
@Override
public void cancel(int rowIndex) {
serverInitiated = true;
GridConnector.this.getWidget().cancelEditor();
}
@Override
public void confirmBind(final boolean bindSucceeded) {
endRequest(bindSucceeded, null, null);
}
@Override
public void confirmSave(boolean saveSucceeded,
String errorMessage, List errorColumnsIds) {
endRequest(saveSucceeded, errorMessage, errorColumnsIds);
}
});
}
@Override
public void bind(EditorRequest request) {
startRequest(request);
rpc.bind(request.getRowIndex());
}
@Override
public void save(EditorRequest request) {
startRequest(request);
rpc.save(request.getRowIndex());
}
@Override
public void cancel(EditorRequest request) {
if (!handleServerInitiated(request)) {
// No startRequest as we don't get (or need)
// a confirmation from the server
rpc.cancel(request.getRowIndex());
}
}
@Override
public Widget getWidget(Grid.Column, JsonObject> column) {
assert column != null;
if (column instanceof CustomGridColumn) {
AbstractComponentConnector c = ((CustomGridColumn) column)
.getEditorConnector();
if (c == null) {
return null;
}
return c.getWidget();
} else {
throw new IllegalStateException("Unexpected column type: "
+ column.getClass().getName());
}
}
/**
* Used to handle the case where the editor calls us because it was
* invoked by the server via RPC and not by the client. In that case,
* the request can be simply synchronously completed.
*
* @param request
* the request object
* @return true if the request was originally triggered by the server,
* false otherwise
*/
private boolean handleServerInitiated(EditorRequest> request) {
assert request != null : "Cannot handle null request";
assert currentRequest == null : "Earlier request not yet finished";
if (serverInitiated) {
serverInitiated = false;
request.success();
return true;
} else {
return false;
}
}
private void startRequest(EditorRequest request) {
assert currentRequest == null : "Earlier request not yet finished";
currentRequest = request;
}
private void endRequest(boolean succeeded, String errorMessage,
List errorColumnsIds) {
assert currentRequest != null : "Current request was null";
/*
* Clear current request first to ensure the state is valid if
* another request is made in the callback.
*/
EditorRequest request = currentRequest;
currentRequest = null;
if (succeeded) {
request.success();
} else {
Collection> errorColumns;
if (errorColumnsIds != null) {
errorColumns = new ArrayList>();
for (String colId : errorColumnsIds) {
errorColumns.add(columnIdToColumn.get(colId));
}
} else {
errorColumns = null;
}
request.failure(errorMessage, errorColumns);
}
}
}
private class ItemClickHandler
implements BodyClickHandler, BodyDoubleClickHandler {
@Override
public void onClick(GridClickEvent event) {
if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
fireItemClick(event.getTargetCell(), event.getNativeEvent());
}
}
@Override
public void onDoubleClick(GridDoubleClickEvent event) {
if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
fireItemClick(event.getTargetCell(), event.getNativeEvent());
}
}
private void fireItemClick(CellReference> cell,
NativeEvent mouseEvent) {
String rowKey = getRowKey((JsonObject) cell.getRow());
String columnId = getColumnId(cell.getColumn());
getRpcProxy(GridServerRpc.class).itemClick(rowKey, columnId,
MouseEventDetailsBuilder
.buildMouseEventDetails(mouseEvent));
}
}
private ColumnReorderHandler columnReorderHandler = new ColumnReorderHandler() {
@Override
public void onColumnReorder(ColumnReorderEvent event) {
if (!columnsUpdatedFromState) {
List> columns = getWidget().getColumns();
final List newColumnOrder = new ArrayList();
for (Column, JsonObject> column : columns) {
if (column instanceof CustomGridColumn) {
newColumnOrder.add(((CustomGridColumn) column).id);
} // the other case would be the multi selection column
}
getRpcProxy(GridServerRpc.class)
.columnsReordered(newColumnOrder, columnOrder);
columnOrder = newColumnOrder;
getState().columnOrder = newColumnOrder;
}
}
};
private ColumnVisibilityChangeHandler columnVisibilityChangeHandler = new ColumnVisibilityChangeHandler() {
@Override
public void onVisibilityChange(
ColumnVisibilityChangeEvent event) {
if (!columnsUpdatedFromState) {
Column, JsonObject> column = event.getColumn();
if (column instanceof CustomGridColumn) {
getRpcProxy(GridServerRpc.class).columnVisibilityChanged(
((CustomGridColumn) column).id, column.isHidden(),
event.isUserOriginated());
for (GridColumnState state : getState().columns) {
if (state.id.equals(((CustomGridColumn) column).id)) {
state.hidden = event.isHidden();
break;
}
}
} else {
getLogger().warning(
"Visibility changed for a unknown column type in Grid: "
+ column.toString() + ", type "
+ column.getClass());
}
}
}
};
private ColumnResizeHandler columnResizeHandler = new ColumnResizeHandler() {
@Override
public void onColumnResize(ColumnResizeEvent event) {
if (!columnsUpdatedFromState) {
Column, JsonObject> column = event.getColumn();
if (column instanceof CustomGridColumn) {
getRpcProxy(GridServerRpc.class).columnResized(
((CustomGridColumn) column).id,
column.getWidthActual());
}
}
}
};
private class CustomDetailsGenerator
implements HeightAwareDetailsGenerator {
private final Map idToDetailsMap = new HashMap();
private final Map idToRowIndex = new HashMap();
private final Map elementToResizeCommand = new HashMap();
private final ElementResizeListener detailsRowResizeListener = new ElementResizeListener() {
@Override
public void onElementResize(ElementResizeEvent e) {
if (elementToResizeCommand.containsKey(e.getElement())) {
Scheduler.get().scheduleFinally(
elementToResizeCommand.get(e.getElement()));
}
}
};
/* calculated when the first details row is opened */
private Double spacerCellBorderHeights = null;
@Override
public Widget getDetails(int rowIndex) {
String id = getId(rowIndex);
if (id == null || !hasDetailsOpen(rowIndex)) {
return null;
}
ComponentConnector componentConnector = idToDetailsMap.get(id);
idToRowIndex.put(id, rowIndex);
Widget widget = componentConnector.getWidget();
getLayoutManager().addElementResizeListener(widget.getElement(),
detailsRowResizeListener);
elementToResizeCommand.put(widget.getElement(),
createResizeCommand(rowIndex, widget.getElement()));
return widget;
}
private ScheduledCommand createResizeCommand(final int rowIndex,
final Element element) {
return new ScheduledCommand() {
@Override
public void execute() {
// It should not be possible to get here without calculating
// the spacerCellBorderHeights or without having the details
// row open, nor for this command to be triggered while
// layout is running, but it's safer to check anyway.
if (spacerCellBorderHeights != null
&& !getLayoutManager().isLayoutRunning()
&& hasDetailsOpen(rowIndex)) {
// Measure and set details height if element is visible
if (WidgetUtil.isDisplayed(element)) {
double height =
getLayoutManager().getOuterHeightDouble(
element) + spacerCellBorderHeights;
getWidget().setDetailsHeight(rowIndex, height);
}
}
}
};
}
private boolean hasDetailsOpen(int rowIndex) {
JsonObject row = getWidget().getDataSource().getRow(rowIndex);
if (row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)) {
String id = row.getString(GridState.JSONKEY_DETAILS_VISIBLE);
return id != null && !id.isEmpty();
}
return false;
}
@Override
public double getDetailsHeight(int rowIndex) {
// Case of null is handled in the getDetails method and this method
// will not called if it returns null.
String id = getId(rowIndex);
ComponentConnector componentConnector = idToDetailsMap.get(id);
getLayoutManager().setNeedsMeasureRecursively(componentConnector);
getLayoutManager().layoutNow();
Element element = componentConnector.getWidget().getElement();
if (spacerCellBorderHeights == null) {
// If theme is changed, new details generator is created from
// scratch, so this value doesn't need to be updated elsewhere.
spacerCellBorderHeights = WidgetUtil
.getBorderTopAndBottomThickness(
element.getParentElement());
}
return getLayoutManager().getOuterHeightDouble(element);
}
/**
* Fetches id from the row object that corresponds with the given
* rowIndex.
*
* @since 7.6.1
* @param rowIndex
* the index of the row for which to fetch the id
* @return id of the row if such id exists, {@code null} otherwise
*/
private String getId(int rowIndex) {
JsonObject row = getWidget().getDataSource().getRow(rowIndex);
if (!row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE) || row
.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty()) {
return null;
}
return row.getString(GridState.JSONKEY_DETAILS_VISIBLE);
}
public void updateConnectorHierarchy(List children) {
Set connectorIds = new HashSet();
for (ServerConnector child : children) {
if (child instanceof ComponentConnector) {
connectorIds.add(child.getConnectorId());
idToDetailsMap.put(child.getConnectorId(),
(ComponentConnector) child);
}
}
Set removedDetails = new HashSet();
for (Entry entry : idToDetailsMap
.entrySet()) {
ComponentConnector connector = entry.getValue();
String id = connector.getConnectorId();
if (!connectorIds.contains(id)) {
removedDetails.add(entry.getKey());
if (idToRowIndex.containsKey(id)) {
getWidget().setDetailsVisible(idToRowIndex.get(id),
false);
}
}
}
for (String id : removedDetails) {
Element element = idToDetailsMap.get(id).getWidget()
.getElement();
elementToResizeCommand.remove(element);
getLayoutManager().removeElementResizeListener(element,
detailsRowResizeListener);
idToDetailsMap.remove(id);
idToRowIndex.remove(id);
}
}
}
/**
* Class for handling scrolling issues with open details.
*
* @since 7.5.2
*/
private class LazyDetailsScroller implements DeferredWorker {
/* Timer value tested to work in our test cluster with slow IE8s. */
private static final int DISABLE_LAZY_SCROLL_TIMEOUT = 1500;
/*
* Cancels details opening scroll after timeout. Avoids any unexpected
* scrolls via details opening.
*/
private Timer disableScroller = new Timer() {
@Override
public void run() {
targetRow = -1;
}
};
private Integer targetRow = -1;
private ScrollDestination destination = null;
public void scrollToRow(Integer row, ScrollDestination dest) {
targetRow = row;
destination = dest;
disableScroller.schedule(DISABLE_LAZY_SCROLL_TIMEOUT);
}
/**
* Inform LazyDetailsScroller that a details row has opened on a row.
*
* @param rowIndex
* index of row with details now open
*/
public void detailsOpened(int rowIndex) {
if (targetRow == rowIndex) {
getWidget().scrollToRow(targetRow, destination);
disableScroller.run();
}
}
@Override
public boolean isWorkPending() {
return disableScroller.isRunning();
}
}
/**
* Maps a generated column id to a grid column instance
*/
private Map columnIdToColumn = new HashMap();
private List columnOrder = new ArrayList();
/**
* {@link #columnsUpdatedFromState} is set to true when
* {@link #updateColumnOrderFromState(List)} is updating the column order
* for the widget. This flag tells the {@link #columnReorderHandler} to not
* send same data straight back to server. After updates, listener sets the
* value back to false.
*/
private boolean columnsUpdatedFromState;
private RpcDataSource dataSource;
/* Used to track Grid editor columns with validation errors */
private final Map, String> columnToErrorMessage = new HashMap, String>();
private ItemClickHandler itemClickHandler = new ItemClickHandler();
private String lastKnownTheme = null;
private final CustomDetailsGenerator customDetailsGenerator = new CustomDetailsGenerator();
private final CustomStyleGenerator styleGenerator = new CustomStyleGenerator();
private final DetailsListener detailsListener = new DetailsListener() {
@Override
public void reapplyDetailsVisibility(final int rowIndex,
final JsonObject row) {
if (hasDetailsOpen(row)) {
// Command for opening details row.
ScheduledCommand openDetails = new ScheduledCommand() {
@Override
public void execute() {
// Re-apply to force redraw.
getWidget().setDetailsVisible(rowIndex, false);
getWidget().setDetailsVisible(rowIndex, true);
lazyDetailsScroller.detailsOpened(rowIndex);
}
};
if (initialChange) {
Scheduler.get().scheduleDeferred(openDetails);
} else {
Scheduler.get().scheduleFinally(openDetails);
}
} else {
getWidget().setDetailsVisible(rowIndex, false);
}
}
private boolean hasDetailsOpen(JsonObject row) {
return row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
&& row.getString(GridState.JSONKEY_DETAILS_VISIBLE) != null;
}
};
private final LazyDetailsScroller lazyDetailsScroller = new LazyDetailsScroller();
private final CustomEditorHandler editorHandler = new CustomEditorHandler();
/*
* Initially details need to behave a bit differently to allow some
* escalator magic.
*/
private boolean initialChange;
@Override
@SuppressWarnings("unchecked")
public Grid getWidget() {
return (Grid) super.getWidget();
}
@Override
public GridState getState() {
return (GridState) super.getState();
}
@Override
protected void init() {
super.init();
Grid grid = getWidget();
// All scroll RPC calls are executed finally to avoid issues on init
registerRpc(GridClientRpc.class, new GridClientRpc() {
@Override
public void scrollToStart() {
/*
* no need for lazyDetailsScrollAdjuster, because the start is
* always 0, won't change a bit.
*/
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
grid.scrollToStart();
}
});
}
@Override
public void scrollToEnd() {
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
grid.scrollToEnd();
// Scrolls further if details opens.
lazyDetailsScroller.scrollToRow(dataSource.size() - 1,
ScrollDestination.END);
}
});
}
@Override
public void scrollToRow(final int row,
final ScrollDestination destination) {
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
grid.scrollToRow(row, destination);
// Scrolls a bit further if details opens.
lazyDetailsScroller.scrollToRow(row, destination);
}
});
}
@Override
public void recalculateColumnWidths() {
grid.recalculateColumnWidths();
}
});
/* Item click events */
grid.addBodyClickHandler(itemClickHandler);
grid.addBodyDoubleClickHandler(itemClickHandler);
/* Style Generators */
grid.setCellStyleGenerator(styleGenerator);
grid.setRowStyleGenerator(styleGenerator);
grid.addSortHandler(new SortHandler() {
@Override
public void sort(SortEvent event) {
List order = event.getOrder();
String[] columnIds = new String[order.size()];
SortDirection[] directions = new SortDirection[order.size()];
for (int i = 0; i < order.size(); i++) {
SortOrder sortOrder = order.get(i);
CustomGridColumn column = (CustomGridColumn) sortOrder
.getColumn();
columnIds[i] = column.id;
directions[i] = sortOrder.getDirection();
}
if (!Arrays.equals(columnIds, getState().sortColumns)
|| !Arrays.equals(directions, getState().sortDirs)) {
// Report back to server if changed
getRpcProxy(GridServerRpc.class).sort(columnIds, directions,
event.isUserOriginated());
}
}
});
grid.setEditorHandler(editorHandler);
grid.addColumnReorderHandler(columnReorderHandler);
grid.addColumnVisibilityChangeHandler(
columnVisibilityChangeHandler);
grid.addColumnResizeHandler(columnResizeHandler);
ConnectorFocusAndBlurHandler.addHandlers(this);
grid.setDetailsGenerator(customDetailsGenerator);
getLayoutManager().registerDependency(this, grid.getElement());
// Handling row height changes
grid.addRowHeightChangedHandler(new RowHeightChangedHandler() {
@Override
public void onRowHeightChanged(RowHeightChangedEvent event) {
getLayoutManager()
.setNeedsMeasureRecursively(GridConnector.this);
getLayoutManager().layoutNow();
}
});
layout();
}
@Override
public void onStateChanged(final StateChangeEvent stateChangeEvent) {
super.onStateChanged(stateChangeEvent);
initialChange = stateChangeEvent.isInitialStateChange();
// Column updates
if (stateChangeEvent.hasPropertyChanged("columns")) {
// Remove old columns
purgeRemovedColumns();
// Update all columns
updateColumnsFromState();
}
if (stateChangeEvent.hasPropertyChanged("columnOrder")) {
if (orderNeedsUpdate(getState().columnOrder)) {
updateColumnOrderFromState(getState().columnOrder);
}
}
// Column resize mode
if (stateChangeEvent.hasPropertyChanged("columnResizeMode")) {
getWidget().setColumnResizeMode(getState().columnResizeMode);
}
// Header and footer
if (stateChangeEvent.hasPropertyChanged("header")) {
updateHeaderFromState(getState().header);
}
if (stateChangeEvent.hasPropertyChanged("footer")) {
updateFooterFromState(getState().footer);
}
// Sorting
if (stateChangeEvent.hasPropertyChanged("sortColumns")
|| stateChangeEvent.hasPropertyChanged("sortDirs")) {
onSortStateChange();
}
// Editor
if (stateChangeEvent.hasPropertyChanged("editorEnabled")
|| stateChangeEvent.hasPropertyChanged("readOnly")) {
getWidget().setEditorEnabled(
getState().editorEnabled && !isReadOnly());
}
// Frozen columns
if (stateChangeEvent.hasPropertyChanged("frozenColumnCount")) {
getWidget().setFrozenColumnCount(getState().frozenColumnCount);
}
// Theme features
String activeTheme = getConnection().getUIConnector().getActiveTheme();
if (lastKnownTheme == null) {
lastKnownTheme = activeTheme;
} else if (!lastKnownTheme.equals(activeTheme)) {
getWidget().resetSizesFromDom();
lastKnownTheme = activeTheme;
}
}
private void updateColumnOrderFromState(List stateColumnOrder) {
CustomGridColumn[] columns = new CustomGridColumn[stateColumnOrder
.size()];
int i = 0;
for (String id : stateColumnOrder) {
columns[i] = columnIdToColumn.get(id);
i++;
}
columnsUpdatedFromState = true;
getWidget().setColumnOrder(columns);
columnsUpdatedFromState = false;
columnOrder = stateColumnOrder;
}
private boolean orderNeedsUpdate(List stateColumnOrder) {
if (stateColumnOrder.size() == columnOrder.size()) {
for (int i = 0; i < columnOrder.size(); ++i) {
if (!stateColumnOrder.get(i).equals(columnOrder.get(i))) {
return true;
}
}
return false;
}
return true;
}
private void updateHeaderFromState(GridStaticSectionState state) {
Grid grid = getWidget();
grid.setHeaderVisible(state.visible);
while (grid.getHeaderRowCount() > 0) {
grid.removeHeaderRow(0);
}
for (RowState rowState : state.rows) {
HeaderRow row = grid.appendHeaderRow();
if (rowState.defaultRow) {
grid.setDefaultHeaderRow(row);
}
for (CellState cellState : rowState.cells) {
CustomGridColumn column = columnIdToColumn
.get(cellState.columnId);
updateHeaderCellFromState(row.getCell(column), cellState);
}
for (Set group : rowState.cellGroups.keySet()) {
Grid.Column, ?>[] columns = new Grid.Column, ?>[group
.size()];
CellState cellState = rowState.cellGroups.get(group);
int i = 0;
for (String columnId : group) {
columns[i] = columnIdToColumn.get(columnId);
i++;
}
// Set state to be the same as first in group.
updateHeaderCellFromState(row.join(columns), cellState);
}
row.setStyleName(rowState.styleName);
}
}
private void updateHeaderCellFromState(HeaderCell cell,
CellState cellState) {
switch (cellState.type) {
case TEXT:
cell.setText(cellState.text);
break;
case HTML:
cell.setHtml(cellState.html);
break;
case WIDGET:
ComponentConnector connector = (ComponentConnector) cellState.connector;
if (connector != null) {
cell.setWidget(connector.getWidget());
} else {
// This happens if you do setVisible(false) on the component on
// the server side
cell.setWidget(null);
}
break;
default:
throw new IllegalStateException(
"unexpected cell type: " + cellState.type);
}
cell.setStyleName(cellState.styleName);
}
private void updateFooterFromState(GridStaticSectionState state) {
Grid grid = getWidget();
grid.setFooterVisible(state.visible);
while (grid.getFooterRowCount() > 0) {
grid.removeFooterRow(0);
}
for (RowState rowState : state.rows) {
FooterRow row = grid.appendFooterRow();
for (CellState cellState : rowState.cells) {
CustomGridColumn column = columnIdToColumn
.get(cellState.columnId);
updateFooterCellFromState(row.getCell(column), cellState);
}
for (Set group : rowState.cellGroups.keySet()) {
Grid.Column, ?>[] columns = new Grid.Column, ?>[group
.size()];
CellState cellState = rowState.cellGroups.get(group);
int i = 0;
for (String columnId : group) {
columns[i] = columnIdToColumn.get(columnId);
i++;
}
// Set state to be the same as first in group.
updateFooterCellFromState(row.join(columns), cellState);
}
row.setStyleName(rowState.styleName);
}
}
private void updateFooterCellFromState(FooterCell cell,
CellState cellState) {
switch (cellState.type) {
case TEXT:
cell.setText(cellState.text);
break;
case HTML:
cell.setHtml(cellState.html);
break;
case WIDGET:
ComponentConnector connector = (ComponentConnector) cellState.connector;
if (connector != null) {
cell.setWidget(connector.getWidget());
} else {
// This happens if you do setVisible(false) on the component on
// the server side
cell.setWidget(null);
}
break;
default:
throw new IllegalStateException(
"unexpected cell type: " + cellState.type);
}
cell.setStyleName(cellState.styleName);
}
/**
* Update columns from the current state.
*
*/
private void updateColumnsFromState() {
this.columnsUpdatedFromState = true;
final List> columns = new ArrayList>(getState().columns.size());
for (String columnId : getState().columnOrder) {
for (GridColumnState state : getState().columns) {
if (state.id.equals(columnId)) {
CustomGridColumn column = this.columnIdToColumn.get(state.id);
if (column == null) {
column = new CustomGridColumn(state);
this.columnIdToColumn.put(state.id, column);
this.columnOrder.add(state.id);
columns.add(column);
} else {
updateColumnFromState(column, state);
}
}
}
}
@SuppressWarnings("unchecked")
final Column, JsonObject>[] columnArray = columns.toArray(new Column[0]);
getWidget().addColumns(columnArray);
this.columnsUpdatedFromState = false;
}
/**
* Updates the column values from a state
*
* @param column
* The column to update
* @param state
* The state to get the data from
*/
@SuppressWarnings("unchecked")
private static void updateColumnFromState(CustomGridColumn column,
GridColumnState state) {
column.setWidth(state.width);
column.setMinimumWidth(state.minWidth);
column.setMaximumWidth(state.maxWidth);
column.setExpandRatio(state.expandRatio);
assert state.rendererConnector instanceof AbstractGridRendererConnector : "GridColumnState.rendererConnector is invalid (not subclass of "
+ "AbstractRendererConnector)";
column.setRenderer(
(AbstractGridRendererConnector) state.rendererConnector);
column.setSortable(state.sortable);
column.setResizable(state.resizable);
column.setHeaderCaption(state.headerCaption);
column.setHidden(state.hidden);
column.setHidable(state.hidable);
column.setHidingToggleCaption(state.hidingToggleCaption);
column.setEditable(state.editable);
column.setEditorConnector(
(AbstractComponentConnector) state.editorConnector);
}
/**
* Removes any orphan columns that has been removed from the state from the
* grid
*/
private void purgeRemovedColumns() {
// Get columns still registered in the state
Set columnsInState = new HashSet();
for (GridColumnState columnState : getState().columns) {
columnsInState.add(columnState.id);
}
// Remove column no longer in state
Iterator columnIdIterator = columnIdToColumn.keySet()
.iterator();
while (columnIdIterator.hasNext()) {
String id = columnIdIterator.next();
if (!columnsInState.contains(id)) {
CustomGridColumn column = columnIdToColumn.get(id);
columnIdIterator.remove();
getWidget().removeColumn(column);
columnOrder.remove(id);
}
}
}
public void setDataSource(RpcDataSource dataSource) {
this.dataSource = dataSource;
getWidget().setDataSource(this.dataSource);
}
private void onSortStateChange() {
List sortOrder = new ArrayList();
String[] sortColumns = getState().sortColumns;
SortDirection[] sortDirs = getState().sortDirs;
for (int i = 0; i < sortColumns.length; i++) {
sortOrder.add(new SortOrder(columnIdToColumn.get(sortColumns[i]),
sortDirs[i]));
}
getWidget().setSortOrder(sortOrder);
}
private Logger getLogger() {
return Logger.getLogger(getClass().getName());
}
/**
* Gets the row key for a row object.
*
* @param row
* the row object
* @return the key for the given row
*/
public String getRowKey(JsonObject row) {
final Object key = dataSource.getRowKey(row);
assert key instanceof String : "Internal key was not a String but a "
+ key.getClass().getSimpleName() + " (" + key + ")";
return (String) key;
}
/*
* (non-Javadoc)
*
* @see
* com.vaadin.client.HasComponentsConnector#updateCaption(com.vaadin.client
* .ComponentConnector)
*/
@Override
public void updateCaption(ComponentConnector connector) {
// TODO Auto-generated method stub
}
@Override
public void onConnectorHierarchyChange(
ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) {
customDetailsGenerator.updateConnectorHierarchy(getChildren());
}
public String getColumnId(Grid.Column, ?> column) {
if (column instanceof CustomGridColumn) {
return ((CustomGridColumn) column).id;
}
return null;
}
@Override
public void layout() {
getWidget().onResize();
}
@Override
public boolean isWorkPending() {
return lazyDetailsScroller.isWorkPending();
}
/**
* Gets the listener used by this connector for tracking when row detail
* visibility changes.
*
* @since 7.5.0
* @return the used details listener
*/
public DetailsListener getDetailsListener() {
return detailsListener;
}
/**
* Checks if the Grid is in read-only mode.
*
* @since 8.22
*
* @return {@code true} if read-only, {@code false} otherwise
*/
public boolean isReadOnly() {
return getState().readOnly;
}
@Override
public boolean hasTooltip() {
return getState().hasDescriptions || super.hasTooltip();
}
@Override
public TooltipInfo getTooltipInfo(Element element) {
CellReference cell = getWidget().getCellReference(element);
if (cell != null) {
JsonObject row = cell.getRow();
if (row == null) {
return null;
}
Column, JsonObject> column = cell.getColumn();
if (!(column instanceof CustomGridColumn)) {
// Selection checkbox column
return null;
}
CustomGridColumn c = (CustomGridColumn) column;
JsonObject cellDescriptions = row
.getObject(GridState.JSONKEY_CELLDESCRIPTION);
if (cellDescriptions != null && cellDescriptions.hasKey(c.id)) {
return createCellTooltipInfo(cellDescriptions.getString(c.id),
getState().cellTooltipContentMode);
} else if (row.hasKey(GridState.JSONKEY_ROWDESCRIPTION)) {
return createCellTooltipInfo(
row.getString(GridState.JSONKEY_ROWDESCRIPTION),
getState().rowTooltipContentMode);
} else {
return null;
}
}
return super.getTooltipInfo(element);
}
private static TooltipInfo createCellTooltipInfo(String text,
ContentMode contentMode) {
TooltipInfo info = new TooltipInfo(text);
info.setContentMode(contentMode);
return info;
}
@Override
protected void sendContextClickEvent(MouseEventDetails details,
EventTarget eventTarget) {
// if element is the resize indicator, ignore the event
if (isResizeHandle(eventTarget)) {
WidgetUtil.clearTextSelection();
return;
}
EventCellReference eventCell = getWidget().getEventCell();
Section section = eventCell.getSection();
String rowKey = null;
if (eventCell.isBody() && eventCell.getRow() != null) {
rowKey = getRowKey(eventCell.getRow());
}
String columnId = getColumnId(eventCell.getColumn());
getRpcProxy(GridServerRpc.class).contextClick(eventCell.getRowIndex(),
rowKey, columnId, section, details);
WidgetUtil.clearTextSelection();
}
private boolean isResizeHandle(EventTarget eventTarget) {
if (Element.is(eventTarget)) {
Element e = Element.as(eventTarget);
if (e.getClassName().contains("-column-resize-handle")) {
return true;
}
}
return false;
}
/**
* Creates a concatenation of all columns errors for Editor.
*
* @since 7.6
* @return displayed error string
*/
private String getColumnErrors() {
List errors = new ArrayList();
for (Grid.Column, JsonObject> c : getWidget().getColumns()) {
if (!(c instanceof CustomGridColumn)) {
continue;
}
String error = columnToErrorMessage.get(c);
if (error != null) {
errors.add(error);
}
}
String result = "";
Iterator i = errors.iterator();
while (i.hasNext()) {
result += i.next();
if (i.hasNext()) {
result += ", ";
}
}
return result.isEmpty() ? null : result;
}
}