package com.vaadin.addon.spreadsheet.client;
/*
* #%L
* Vaadin Spreadsheet
* %%
* Copyright (C) 2013 - 2015 Vaadin Ltd
* %%
* This program is available under Commercial Vaadin Add-On License 3.0
* (CVALv3).
*
* See the file license.html distributed with this software for more
* information about licensing.
*
* You should have received a copy of the CVALv3 along with this program.
* If not, see <http://vaadin.com/license/cval-3>.
* #L%
*/
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.gwt.core.client.GWT;
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.event.dom.client.ContextMenuEvent;
import com.google.gwt.event.dom.client.ContextMenuHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.addon.spreadsheet.Spreadsheet;
import com.vaadin.addon.spreadsheet.client.SpreadsheetWidget.SheetContextMenuHandler;
import com.vaadin.addon.spreadsheet.shared.SpreadsheetState;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractHasComponentsConnector;
import com.vaadin.client.ui.Action;
import com.vaadin.client.ui.ActionOwner;
import com.vaadin.client.ui.PostLayoutListener;
import com.vaadin.client.ui.layout.ElementResizeEvent;
import com.vaadin.client.ui.layout.ElementResizeListener;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.Connect.LoadStyle;
@SuppressWarnings("serial")
@Connect(value = Spreadsheet.class, loadStyle = LoadStyle.DEFERRED)
public class SpreadsheetConnector extends AbstractHasComponentsConnector
implements PostLayoutListener {
SpreadsheetClientRpc clientRPC = new SpreadsheetClientRpc() {
@Override
public void showCellValue(String value, int col, int row,
boolean formula, boolean locked) {
getWidget().showCellValue(value, col, row, formula, locked);
}
@Override
public void showSelectedCell(int col, int row, String value,
boolean formula, boolean locked, boolean initialSelection) {
getWidget().selectCell(col, row, value, formula, locked,
initialSelection);
}
@Override
public void invalidCellAddress() {
getWidget().invalidCellAddress();
}
@Override
public void showSelectedCellRange(int firstColumn, int lastColumn,
int firstRow, int lastRow, String value, boolean formula,
boolean locked) {
getWidget()
.selectCellRange(firstColumn, firstRow, firstColumn,
lastColumn, firstRow, lastRow, value, formula,
locked, true);
}
@Override
public void showActions(
final ArrayList<SpreadsheetActionDetails> actionDetails) {
int left;
int top;
if (latestCellContextMenuEvent != null) {
left = SpreadsheetWidget
.getTouchOrMouseClientX(latestCellContextMenuEvent);
top = SpreadsheetWidget
.getTouchOrMouseClientY(latestCellContextMenuEvent);
} else {
left = SpreadsheetWidget
.getTouchOrMouseClientX(latestHeaderContextMenuEvent);
top = SpreadsheetWidget
.getTouchOrMouseClientY(latestHeaderContextMenuEvent);
}
top += Window.getScrollTop();
left += Window.getScrollLeft();
getConnection().getContextMenu().showAt(new ActionOwner() {
@Override
public String getPaintableId() {
return SpreadsheetConnector.this.getConnectorId();
}
@Override
public ApplicationConnection getClient() {
return SpreadsheetConnector.this.getConnection();
}
@Override
public Action[] getActions() {
List<Action> actions = new ArrayList<Action>();
SpreadsheetWidget widget = getWidget();
SpreadsheetServerRpc rpcProxy = getRpcProxy(SpreadsheetServerRpc.class);
for (SpreadsheetActionDetails actionDetail : actionDetails) {
SpreadsheetAction spreadsheetAction = new SpreadsheetAction(
this, rpcProxy, actionDetail.key,
actionDetail.type, widget);
spreadsheetAction.setCaption(actionDetail.caption);
spreadsheetAction
.setIconUrl(getResourceUrl(actionDetail.key));
actions.add(spreadsheetAction);
}
return actions.toArray(new Action[actions.size()]);
}
}, left, top);
}
@Override
public void setSelectedCellAndRange(int col, int row, int c1, int c2,
int r1, int r2, String value, boolean formula,
boolean cellLocked, boolean scroll) {
getWidget().selectCellRange(col, row, c1, c2, r1, r2, value,
formula, cellLocked, scroll);
}
@Override
public void updateBottomRightCellValues(ArrayList<CellData> cellData) {
getWidget().updateBottomRightCellValues(cellData);
}
@Override
public void updateTopLeftCellValues(ArrayList<CellData> cellData) {
getWidget().updateTopLeftCellValues(cellData);
}
@Override
public void updateTopRightCellValues(ArrayList<CellData> cellData) {
getWidget().updateTopRightCellValues(cellData);
}
@Override
public void updateBottomLeftCellValues(ArrayList<CellData> cellData) {
getWidget().updateBottomLeftCellValues(cellData);
}
@Override
public void cellsUpdated(ArrayList<CellData> updatedCellData) {
getWidget().cellValuesUpdated(updatedCellData);
}
@Override
public void refreshCellStyles() {
getWidget().refreshCellStyles();
}
@Override
public void editCellComment(int col, int row) {
getWidget().editCellComment(col, row);
}
};
private final ElementResizeListener elementResizeListener = new ElementResizeListener() {
@Override
public void onElementResize(ElementResizeEvent e) {
getWidget().widgetSizeChanged();
}
};
private SpreadsheetCustomEditorFactory customEditorFactory;
private NativeEvent latestCellContextMenuEvent;
private NativeEvent latestHeaderContextMenuEvent;
private List<String> visibleCellCommentKeys = new ArrayList<String>();
private Set<String> currentOverlays = new HashSet<String>();
@Override
protected void init() {
super.init();
getWidget().setId(getConnectorId());
registerRpc(SpreadsheetClientRpc.class, clientRPC);
getWidget().setCommsTrigger(new CommsTrigger() {
@Override
public void sendUpdates() {
getConnection().sendPendingVariableChanges();
}
});
getWidget().setSpreadsheetHandler(
getRpcProxy(SpreadsheetServerRpc.class));
getWidget().setSheetContextMenuHandler(new SheetContextMenuHandler() {
@Override
public void cellContextMenu(NativeEvent event, int column, int row) {
if (getState().hasActions) {
latestCellContextMenuEvent = event;
latestHeaderContextMenuEvent = null;
getRpcProxy(SpreadsheetServerRpc.class)
.contextMenuOpenOnSelection(row, column);
}
}
@Override
public void rowHeaderContextMenu(NativeEvent nativeEvent,
int rowIndex) {
if (getState().hasActions) {
latestHeaderContextMenuEvent = nativeEvent;
latestCellContextMenuEvent = null;
getRpcProxy(SpreadsheetServerRpc.class)
.rowHeaderContextMenuOpen(rowIndex);
}
}
@Override
public void columnHeaderContextMenu(NativeEvent nativeEvent,
int columnIndex) {
if (getState().hasActions) {
latestHeaderContextMenuEvent = nativeEvent;
latestCellContextMenuEvent = null;
getRpcProxy(SpreadsheetServerRpc.class)
.columnHeaderContextMenuOpen(columnIndex);
}
}
});
// Prevent context menu on context menu
getConnection().getContextMenu().addDomHandler(
new ContextMenuHandler() {
@Override
public void onContextMenu(ContextMenuEvent event) {
event.preventDefault();
event.stopPropagation();
}
}, ContextMenuEvent.getType());
getLayoutManager().addElementResizeListener(getWidget().getElement(),
elementResizeListener);
getRpcProxy(SpreadsheetServerRpc.class).onConnectorInit();
}
@Override
public void onUnregister() {
super.onUnregister();
getWidget().clearSpreadsheet(true);
getLayoutManager().removeElementResizeListener(
getWidget().getElement(), elementResizeListener);
if (currentOverlays != null) {
currentOverlays.clear();
}
visibleCellCommentKeys.clear();
}
@Override
protected Widget createWidget() {
return GWT.create(SpreadsheetWidget.class);
}
@Override
public SpreadsheetWidget getWidget() {
return (SpreadsheetWidget) super.getWidget();
}
@Override
public SpreadsheetState getState() {
return (SpreadsheetState) super.getState();
}
@Override
public void onStateChanged(final StateChangeEvent stateChangeEvent) {
super.onStateChanged(stateChangeEvent);
final SpreadsheetWidget widget = getWidget();
SpreadsheetState state = getState();
// in case the component client side is just created, but server side
// has been existing (like when component has been invisible
if (!state.reload && stateChangeEvent.isInitialStateChange()) {
loadInitialStateDataToWidget(stateChangeEvent);
// this is deferred because the first layout of the spreadsheet is
// done as deferred
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
loadStateChangeDataToWidget(stateChangeEvent);
}
});
} else if (state.reload) {
loadInitialStateDataToWidget(stateChangeEvent);
} else {
if (stateChangeEvent.hasPropertyChanged("sheetNames")
|| stateChangeEvent.hasPropertyChanged("sheetIndex")) {
widget.sheetUpdated(state.sheetNames, state.sheetIndex,
stateChangeEvent
.hasPropertyChanged("workbookChangeToggle"));
}
if (stateChangeEvent.hasPropertyChanged("hiddenColumnIndexes")
|| stateChangeEvent.hasPropertyChanged("hiddenRowIndexes")
|| stateChangeEvent.hasPropertyChanged("colW")
|| stateChangeEvent.hasPropertyChanged("rowH")
|| stateChangeEvent.hasPropertyChanged("rows")
|| stateChangeEvent.hasPropertyChanged("cols")
|| stateChangeEvent
.hasPropertyChanged("verticalSplitPosition")
|| stateChangeEvent
.hasPropertyChanged("horizontalSplitPosition")) {
widget.relayoutSheet();
getWidget().updateMergedRegions(getState().mergedRegions);
} else if (stateChangeEvent.hasPropertyChanged("mergedRegions")) {
getWidget().updateMergedRegions(getState().mergedRegions);
}
if (stateChangeEvent.hasPropertyChanged("sheetProtected")) {
widget.setSheetProtected(state.sheetProtected);
}
loadStateChangeDataToWidget(stateChangeEvent);
}
}
private void loadInitialStateDataToWidget(StateChangeEvent stateChangeEvent) {
SpreadsheetState state = getState();
SpreadsheetWidget widget = getWidget();
setupCustomEditors();
widget.sheetUpdated(state.sheetNames, state.sheetIndex,
stateChangeEvent.hasPropertyChanged("workbookChangeToggle"));
widget.setSheetProtected(state.sheetProtected);
widget.load();
widget.updateMergedRegions(state.mergedRegions);
}
private void loadStateChangeDataToWidget(StateChangeEvent stateChangeEvent) {
final SpreadsheetWidget widget = getWidget();
SpreadsheetState state = getState();
if (stateChangeEvent.hasPropertyChanged("componentIDtoCellKeysMap")) {
HashMap<String, String> cellKeysToComponentIdMap = state.componentIDtoCellKeysMap;
HashMap<String, Widget> customWidgetMap = new HashMap<String, Widget>();
if (cellKeysToComponentIdMap != null
&& !cellKeysToComponentIdMap.isEmpty()) {
List<ComponentConnector> childComponents = getChildComponents();
for (ComponentConnector cc : childComponents) {
String connectorId = cc.getConnectorId();
if (cellKeysToComponentIdMap.containsKey(connectorId)) {
customWidgetMap.put(
cellKeysToComponentIdMap.get(connectorId),
cc.getWidget());
}
}
}
widget.showCellCustomComponents(customWidgetMap);
}
if (stateChangeEvent.hasPropertyChanged("cellKeysToEditorIdMap")) {
setupCustomEditors();
}
if (stateChangeEvent.hasPropertyChanged("cellComments")
|| stateChangeEvent.hasPropertyChanged("cellCommentAuthors")) {
widget.setCellComments(state.cellComments, state.cellCommentAuthors);
}
if (stateChangeEvent.hasPropertyChanged("visibleCellComments")) {
setupVisibleCellComments();
}
if (stateChangeEvent.hasPropertyChanged("invalidFormulaCells")) {
widget.setInvalidFormulaCells(state.invalidFormulaCells);
}
if (stateChangeEvent.hasPropertyChanged("overlays")) {
overlaysChange();
}
widget.getSheetWidget().updateSheetPanePositions();
}
private void overlaysChange() {
Map<String, OverlayInfo> overlayInfos = getState().overlays == null ? Collections
.<String, OverlayInfo> emptyMap() : getState().overlays;
removeOldOverlays(overlayInfos.keySet());
addOrUpdateOverlays(overlayInfos);
currentOverlays = overlayInfos.keySet();
}
private void addOrUpdateOverlays(Map<String, OverlayInfo> overlayInfos) {
for (String key : overlayInfos.keySet()) {
if (currentOverlays.contains(key)) {
getWidget().updateOverlay(key, overlayInfos.get(key));
} else {
addOverlay(key, overlayInfos.get(key));
}
}
}
private void addOverlay(String id, OverlayInfo overlayInfo) {
switch (overlayInfo.type) {
case IMAGE:
getWidget().addOverlay(id, new Image(getResourceUrl(id)), overlayInfo);
break;
case COMPONENT:
for (ComponentConnector c : getChildComponents()) {
if (c.getConnectorId().equals(id)) {
getWidget().addOverlay(id, c.getWidget(), overlayInfo);
}
}
break;
}
}
private void removeOldOverlays(Set<String> newOverlayKeys) {
for (String key : currentOverlays) {
if (!newOverlayKeys.contains(key)) {
getWidget().removeOverlay(key);
}
}
}
private void setupCustomEditors() {
if (getState().cellKeysToEditorIdMap == null) {
getWidget().setCustomEditorFactory(null);
} else if (getWidget().getCustomEditorFactory() == null) {
if (customEditorFactory == null) {
customEditorFactory = new SpreadsheetCustomEditorFactory() {
@Override
public boolean hasCustomEditor(String key) {
return getState().cellKeysToEditorIdMap
.containsKey(key);
}
@Override
public Widget getCustomEditor(String key) {
String editorId = getState().cellKeysToEditorIdMap
.get(key);
List<ComponentConnector> childComponents = getChildComponents();
for (ComponentConnector cc : childComponents) {
if (editorId.equals(cc.getConnectorId())) {
return cc.getWidget();
}
}
return null;
}
};
}
getWidget().setCustomEditorFactory(customEditorFactory);
} else {
getWidget().loadSelectedCellEditor();
}
}
private void setupVisibleCellComments() {
List<String> visibleCellComments = getState().visibleCellComments;
SpreadsheetWidget widget = getWidget();
// remove old
for (String key : visibleCellCommentKeys) {
if (!visibleCellComments.contains(key)) {
widget.removeVisibleCellComment(key);
}
}
if (visibleCellComments != null) {
// add new
for (String key : visibleCellComments) {
if (!visibleCellCommentKeys.contains(key)) {
widget.addVisibleCellComment(key);
}
}
}
visibleCellCommentKeys.clear();
if (visibleCellComments != null) {
visibleCellCommentKeys.addAll(visibleCellComments);
}
}
@Override
public void updateCaption(ComponentConnector connector) {
}
@Override
public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
// remove old popup buttons
List<ComponentConnector> oldChildren = event.getOldChildren();
for (ComponentConnector child : oldChildren) {
if (child instanceof PopupButtonConnector
&& child.getParent() != this) {
getWidget().removePopupButton(
((PopupButtonConnector) child).getWidget());
}
}
for (ComponentConnector child : getChildComponents()) {
if (child instanceof PopupButtonConnector
&& !oldChildren.contains(child)) {
getWidget().addPopupButton(
((PopupButtonConnector) child).getWidget());
}
}
}
@Override
public void postLayout() {
getWidget().refreshOverlayPositions();
}
public interface CommsTrigger {
void sendUpdates();
}
}