/*
* Copyright 2000-2016 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.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.shared.MouseEventDetails;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.Connect;
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
* <code>Grid</code> component.
* <p>
* 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<JsonObject>, RowStyleGenerator<JsonObject> {
@Override
public String getStyle(CellReference<JsonObject> 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<JsonObject> 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<Object, JsonObject> {
private final String id;
private AbstractGridRendererConnector<Object> rendererConnector;
private AbstractComponentConnector editorConnector;
private HandlerRegistration errorStateHandler;
public CustomGridColumn(String id,
AbstractGridRendererConnector<Object> rendererConnector) {
super(rendererConnector.getRenderer());
this.rendererConnector = rendererConnector;
this.id = id;
}
/**
* Sets a new renderer for this column object
*
* @param rendererConnector
* a renderer connector object
*/
public void setRenderer(
AbstractGridRendererConnector<Object> 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<JsonObject> {
private EditorServerRpc rpc = getRpcProxy(EditorServerRpc.class);
private EditorRequest<JsonObject> 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() {
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<String> errorColumnsIds) {
endRequest(saveSucceeded, errorMessage, errorColumnsIds);
}
});
}
@Override
public void bind(EditorRequest<JsonObject> request) {
startRequest(request);
rpc.bind(request.getRowIndex());
}
@Override
public void save(EditorRequest<JsonObject> request) {
startRequest(request);
rpc.save(request.getRowIndex());
}
@Override
public void cancel(EditorRequest<JsonObject> 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<JsonObject> request) {
assert currentRequest == null : "Earlier request not yet finished";
currentRequest = request;
}
private void endRequest(boolean succeeded, String errorMessage,
List<String> 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<JsonObject> request = currentRequest;
currentRequest = null;
if (succeeded) {
request.success();
} else {
Collection<Column<?, JsonObject>> errorColumns;
if (errorColumnsIds != null) {
errorColumns = new ArrayList<Column<?, JsonObject>>();
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<JsonObject> columnReorderHandler = new ColumnReorderHandler<JsonObject>() {
@Override
public void onColumnReorder(ColumnReorderEvent<JsonObject> event) {
if (!columnsUpdatedFromState) {
List<Column<?, JsonObject>> columns = getWidget().getColumns();
final List<String> newColumnOrder = new ArrayList<String>();
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<JsonObject> columnVisibilityChangeHandler = new ColumnVisibilityChangeHandler<JsonObject>() {
@Override
public void onVisibilityChange(
ColumnVisibilityChangeEvent<JsonObject> 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<JsonObject> columnResizeHandler = new ColumnResizeHandler<JsonObject>() {
@Override
public void onColumnResize(ColumnResizeEvent<JsonObject> 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<String, ComponentConnector> idToDetailsMap = new HashMap<String, ComponentConnector>();
private final Map<String, Integer> idToRowIndex = new HashMap<String, Integer>();
@Override
public Widget getDetails(int rowIndex) {
String id = getId(rowIndex);
if (id == null) {
return null;
}
ComponentConnector componentConnector = idToDetailsMap.get(id);
idToRowIndex.put(id, rowIndex);
return componentConnector.getWidget();
}
@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();
return getLayoutManager().getOuterHeightDouble(
componentConnector.getWidget().getElement());
}
/**
* 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<ServerConnector> children) {
Set<String> connectorIds = new HashSet<String>();
for (ServerConnector child : children) {
if (child instanceof ComponentConnector) {
connectorIds.add(child.getConnectorId());
idToDetailsMap.put(child.getConnectorId(),
(ComponentConnector) child);
}
}
Set<String> removedDetails = new HashSet<String>();
for (Entry<String, ComponentConnector> 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) {
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<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>();
private List<String> columnOrder = new ArrayList<String>();
/**
* {@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<Column<?, JsonObject>, String> columnToErrorMessage = new HashMap<Column<?, JsonObject>, 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<JsonObject> getWidget() {
return (Grid<JsonObject>) super.getWidget();
}
@Override
public GridState getState() {
return (GridState) super.getState();
}
@Override
protected void init() {
super.init();
// 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() {
getWidget().scrollToStart();
}
});
}
@Override
public void scrollToEnd() {
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
getWidget().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() {
getWidget().scrollToRow(row, destination);
// Scrolls a bit further if details opens.
lazyDetailsScroller.scrollToRow(row, destination);
}
});
}
@Override
public void recalculateColumnWidths() {
getWidget().recalculateColumnWidths();
}
});
/* Item click events */
getWidget().addBodyClickHandler(itemClickHandler);
getWidget().addBodyDoubleClickHandler(itemClickHandler);
/* Style Generators */
getWidget().setCellStyleGenerator(styleGenerator);
getWidget().setRowStyleGenerator(styleGenerator);
getWidget().addSortHandler(new SortHandler<JsonObject>() {
@Override
public void sort(SortEvent<JsonObject> event) {
List<SortOrder> 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());
}
}
});
getWidget().setEditorHandler(editorHandler);
getWidget().addColumnReorderHandler(columnReorderHandler);
getWidget().addColumnVisibilityChangeHandler(
columnVisibilityChangeHandler);
getWidget().addColumnResizeHandler(columnResizeHandler);
ConnectorFocusAndBlurHandler.addHandlers(this);
getWidget().setDetailsGenerator(customDetailsGenerator);
getLayoutManager().registerDependency(this, getWidget().getElement());
// Handling row height changes
getWidget().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();
// Add new columns
for (GridColumnState state : getState().columns) {
if (!columnIdToColumn.containsKey(state.id)) {
addColumnFromStateChangeEvent(state);
}
updateColumnFromStateChangeEvent(state);
}
}
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")) {
getWidget().setEditorEnabled(getState().editorEnabled);
}
// 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<String> 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<String> 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) {
getWidget().setHeaderVisible(state.visible);
while (getWidget().getHeaderRowCount() > 0) {
getWidget().removeHeaderRow(0);
}
for (RowState rowState : state.rows) {
HeaderRow row = getWidget().appendHeaderRow();
if (rowState.defaultRow) {
getWidget().setDefaultHeaderRow(row);
}
for (CellState cellState : rowState.cells) {
CustomGridColumn column = columnIdToColumn
.get(cellState.columnId);
updateHeaderCellFromState(row.getCell(column), cellState);
}
for (Set<String> 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) {
getWidget().setFooterVisible(state.visible);
while (getWidget().getFooterRowCount() > 0) {
getWidget().removeFooterRow(0);
}
for (RowState rowState : state.rows) {
FooterRow row = getWidget().appendFooterRow();
for (CellState cellState : rowState.cells) {
CustomGridColumn column = columnIdToColumn
.get(cellState.columnId);
updateFooterCellFromState(row.getCell(column), cellState);
}
for (Set<String> 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);
}
/**
* Updates a column from a state change event.
*
* @param columnIndex
* The index of the column to update
*/
private void updateColumnFromStateChangeEvent(GridColumnState columnState) {
CustomGridColumn column = columnIdToColumn.get(columnState.id);
columnsUpdatedFromState = true;
updateColumnFromState(column, columnState);
columnsUpdatedFromState = false;
}
/**
* Adds a new column to the grid widget from a state change event
*
* @param columnIndex
* The index of the column, according to how it
*/
private void addColumnFromStateChangeEvent(GridColumnState state) {
@SuppressWarnings("unchecked")
CustomGridColumn column = new CustomGridColumn(state.id,
((AbstractGridRendererConnector<Object>) state.rendererConnector));
columnIdToColumn.put(state.id, column);
/*
* Add column to grid. Reordering is handled as a separate problem.
*/
getWidget().addColumn(column);
columnOrder.add(state.id);
}
/**
* 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 AbstractGridRendererConnector)";
column.setRenderer(
(AbstractGridRendererConnector<Object>) 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<String> columnsInState = new HashSet<String>();
for (GridColumnState columnState : getState().columns) {
columnsInState.add(columnState.id);
}
// Remove column no longer in state
Iterator<String> 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> sortOrder = new ArrayList<SortOrder>();
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;
}
@Override
public boolean hasTooltip() {
return getState().hasDescriptions || super.hasTooltip();
}
@Override
public TooltipInfo getTooltipInfo(Element element) {
CellReference<JsonObject> 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 new TooltipInfo(cellDescriptions.getString(c.id));
} else if (row.hasKey(GridState.JSONKEY_ROWDESCRIPTION)) {
return new TooltipInfo(
row.getString(GridState.JSONKEY_ROWDESCRIPTION));
} else {
return null;
}
}
return super.getTooltipInfo(element);
}
@Override
protected void sendContextClickEvent(MouseEventDetails details,
EventTarget eventTarget) {
// if element is the resize indicator, ignore the event
if (isResizeHandle(eventTarget)) {
WidgetUtil.clearTextSelection();
return;
}
EventCellReference<JsonObject> 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<String> errors = new ArrayList<String>();
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<String> i = errors.iterator();
while (i.hasNext()) {
result += i.next();
if (i.hasNext()) {
result += ", ";
}
}
return result.isEmpty() ? null : result;
}
}