com.vaadin.addon.spreadsheet.client.SpreadsheetWidget.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.addon.spreadsheet.client.SpreadsheetWidget.java

Source

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.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.TouchEvent;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.addon.spreadsheet.client.MergedRegionUtil.MergedRegionContainer;
import com.vaadin.addon.spreadsheet.client.SheetTabSheet.SheetTabSheetHandler;
import com.vaadin.addon.spreadsheet.client.SpreadsheetConnector.CommsTrigger;
import com.vaadin.addon.spreadsheet.shared.GroupingData;
import com.vaadin.client.Focusable;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.Util;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.RpcProxy;

public class SpreadsheetWidget extends Composite
        implements SheetHandler, FormulaBarHandler, SheetTabSheetHandler, Focusable {

    public interface SheetContextMenuHandler {
        /**
         * Right click (event) on top of the cell at the indexes.
         *
         * @param event
         *            the browser event related (right mouse button click)
         * @param column
         *            1-based, index of cell
         * @param row
         *            1-based, index of cell
         */
        public void cellContextMenu(NativeEvent event, int column, int row);

        /**
         * Right click (event) on top of row header at the index
         *
         * @param nativeEvent
         * @param rowIndex
         *            1-based
         */
        public void rowHeaderContextMenu(NativeEvent nativeEvent, int rowIndex);

        /**
         * Right click (event) on top of column header at the index
         *
         * @param nativeEvent
         * @param columnIndex
         *            1-based
         */
        public void columnHeaderContextMenu(NativeEvent nativeEvent, int columnIndex);
    }

    private static final int DELAYED_SERVER_REQUEST_DELAY = 200; // ms
    private static final String DEFAULT_WIDTH = "500.0px";
    private static final String DEFAULT_HEIGHT = "400.0px";

    private final SheetWidget sheetWidget;
    final FormulaBarWidget formulaBarWidget;
    private final SheetTabSheet sheetTabSheet;

    private final SelectionHandler selectionHandler;

    SpreadsheetHandler spreadsheetHandler;

    private SheetContextMenuHandler sheetContextMenuHandler;

    SpreadsheetCustomEditorFactory customEditorFactory;

    private int rowBufferSize;

    private int columnBufferSize;

    private int rows;

    private int cols;

    private float defRowH;
    private int defColW;

    private float[] rowH;
    private int[] colW;

    /** 1-based */
    private int activeSheetIndex;

    private Map<Integer, String> cellStyleToCSSStyle;
    public Map<Integer, Integer> rowIndexToStyleIndex;
    public Map<Integer, Integer> columnIndexToStyleIndex;
    private Set<Integer> lockedColumnIndexes;
    private Set<Integer> lockedRowIndexes;

    private Map<Integer, String> conditionalFormattingStyles = new HashMap<Integer, String>();

    private boolean loaded;
    private boolean touchMode;
    private boolean formulaBarEditing;
    private boolean inlineEditing;
    private boolean cancelDeferredCommit;
    private boolean selectedCellIsFormulaType;
    boolean cellLocked;
    boolean customCellEditorDisplayed;
    private boolean sheetProtected;
    private boolean cancelNextSheetRelayout;
    private String cachedCellValue;
    private int[] verticalScrollPositions;
    private int[] horizontalScrollPositions;
    // private int firstVisibleTab; Not working in POI -> disabled
    private String[] sheetNames;
    List<Integer> hiddenColumnIndexes;
    List<Integer> hiddenRowIndexes;
    private List<MergedRegion> mergedRegions;
    private boolean lockFormatColumns = true;
    private boolean lockFormatRows = true;

    /**
     * Timer flag for sending lazy RPCs to server. Used so that we don't send an
     * RPC for each key press. Default timeout is a second.
     */
    private boolean okToSendCellProtectRpc = true;

    @SuppressWarnings("serial")
    MergedRegionContainer mergedRegionContainer = new MergedRegionContainer() {

        @Override
        public MergedRegion getMergedRegionStartingFrom(int column, int row) {
            if (mergedRegions != null) {
                for (MergedRegion region : mergedRegions) {
                    if (region.col1 == column && region.row1 == row) {
                        return region;
                    }
                }
            }
            return null;
        }

        @Override
        public MergedRegion getMergedRegion(int column, int row) {
            if (mergedRegions != null) {
                for (MergedRegion region : mergedRegions) {
                    if (region.col1 <= column && region.row1 <= row && region.col2 >= column
                            && region.row2 >= row) {
                        return region;
                    }
                }
            }
            return null;
        }
    };
    private CommsTrigger commsTrigger;

    /**
     * The last click coords when editing formula
     */
    private int tempSelectionStartCol;

    /**
     * The last click coords when editing formula
     */
    private int tempSelectionStartRow;

    public SpreadsheetWidget() {

        setTouchMode(TouchEvent.isSupported());

        sheetWidget = new SheetWidget(this, touchMode);
        formulaBarWidget = new FormulaBarWidget(this, sheetWidget);
        sheetTabSheet = new SheetTabSheet(this);
        selectionHandler = new SelectionHandler(this, sheetWidget);

        sheetWidget.getElement().appendChild(formulaBarWidget.getElement());
        sheetWidget.getElement().appendChild(sheetTabSheet.getElement());

        initWidget(sheetWidget);

        //There is a bug in CssLayout/VerticalLayout.
        //If a component calls setVisible(false) another component in the layout
        //next to it is detached and then attached to the layout and the scroll
        //position is reset. We need to store the scroll position on detach and
        //then set on attach event.
        sheetWidget.addAttachHandler(new AttachEvent.Handler() {
            int leftScrollPosition = 0;
            int topScrollPosition = 0;

            @Override
            public void onAttachOrDetach(AttachEvent attachEvent) {
                if (attachEvent.isAttached()) {
                    sheetWidget.setScrollPosition(leftScrollPosition, topScrollPosition);
                } else {
                    leftScrollPosition = sheetWidget.getSheetScrollLeft();
                    topScrollPosition = sheetWidget.getSheetScrollTop();
                }

            }
        });
    }

    @Override
    public void setHeight(final String height) {
        if (height != null && !height.isEmpty()) {
            super.setHeight(height);
        } else {
            super.setHeight(DEFAULT_HEIGHT);
        }
    }

    @Override
    public void setWidth(final String width) {
        if (width != null && !width.isEmpty()) {
            super.setWidth(width);
        } else {
            super.setWidth(DEFAULT_WIDTH);
        }
    }

    /**
     * Enable or disable Formatting columns locking.
     *
     * @param value
     *            the new content. Can not be HTML.
     */
    public void setLockFormatColumns(boolean enabled) {
        lockFormatColumns = enabled;
        if (lockFormatColumns) {
            if (!getStyleName().contains("lock-format-columns")) {
                addStyleName("lock-format-columns");
            }
        } else {
            removeStyleName("lock-format-columns");
        }
    }

    /**
     * Enable or disable Formatting rows locking.
     *
     * @param value
     *            the new content. Can not be HTML.
     */
    public void setLockFormatRows(boolean enabled) {
        lockFormatRows = enabled;
        if (lockFormatRows) {
            addStyleName("lock-format-rows");
        } else {
            removeStyleName("lock-format-rows");
        }
    }

    /**
     * Sets the content of the info label.
     *
     * @param value
     *            the new content. Can not be HTML.
     */
    public void setInfoLabelValue(String value) {
        sheetTabSheet.setInfoLabelValue(value);
    }

    /**
     * @return current content of the info label.
     */
    public String getInfoLabelValue() {
        return sheetTabSheet.getInfoLabelValue();
    }

    public SheetWidget getSheetWidget() {
        return sheetWidget;
    }

    public void load() {
        if (loaded) {
            clearSpreadsheet(false);
        } else {
            loaded = true;
        }
        loadSheet(activeSheetIndex - 1);
    }

    public void sheetUpdated(String[] sheetNames, int sheetIndex, boolean clearScrollPosition) {
        if (!loaded) { // component first load
            sheetTabSheet.addTabs(sheetNames);
            sheetTabSheet.setSelectedTab(sheetIndex);
        } else {
            if (activeSheetIndex != sheetIndex) {
                // active sheet or whole spreadsheet has changed
                sheetTabSheet.setTabs(sheetNames, clearScrollPosition);
                sheetTabSheet.setSelectedTab(sheetIndex);
            } else if (this.sheetNames == null || !Arrays.equals(this.sheetNames, sheetNames)) {
                // sheet renamed
                sheetTabSheet.setTabs(sheetNames, false);
            }
        }
        this.sheetNames = sheetNames;
        activeSheetIndex = sheetIndex;
    }

    public void widgetSizeChanged() {
        sheetWidget.onWidgetResize();
        sheetTabSheet.onWidgetResize();
    }

    /** Clear all current sheet related data */
    public void clearSpreadsheet(boolean removed) {
        selectionHandler.clearBeforeMergeCells();

        // reset function bar
        formulaBarWidget.clear();
        clearMergedRegions();
        // reset sheet
        sheetWidget.clearAll(removed);
    }

    /**
     *
     * @param sheetIndex
     *            0-based index of the sheet to load
     */
    protected void loadSheet(final int sheetIndex) {
        // load all sheet stuff from model
        final int scrollLeft;
        if (horizontalScrollPositions.length > sheetIndex) {
            scrollLeft = horizontalScrollPositions[sheetIndex];
        } else {
            scrollLeft = 0;
        }
        final int scrollTop;
        if (verticalScrollPositions.length > sheetIndex) {
            scrollTop = verticalScrollPositions[sheetIndex];
        } else {
            scrollTop = 0;
        }
        sheetWidget.resetFromModel(scrollLeft, scrollTop);

        if (scrollLeft != 0 || scrollTop != 0) {
            Scheduler.get().scheduleDeferred(new ScheduledCommand() {

                @Override
                public void execute() {
                    sheetWidget.setScrollPosition(scrollLeft, scrollTop);
                }
            });
        }
    }

    public void relayoutSheet() {
        if (cancelNextSheetRelayout) {
            cancelNextSheetRelayout = false;
        } else {
            sheetWidget.relayoutSheet(true);
        }
    }

    public void setSpreadsheetHandler(SpreadsheetHandler spreadsheetHandler) {
        this.spreadsheetHandler = spreadsheetHandler;
    }

    public void setSheetContextMenuHandler(SheetContextMenuHandler sheetContextMenuHandler) {
        this.sheetContextMenuHandler = sheetContextMenuHandler;
    }

    @Override
    public boolean hasCustomContextMenu() {
        return sheetContextMenuHandler != null;
    }

    public void showCellCustomComponents(HashMap<String, Widget> customWidgetMap) {
        sheetWidget.showCustomWidgets(customWidgetMap);
    }

    public void addPopupButton(PopupButtonWidget widget) {
        sheetWidget.addPopupButton(widget);
    }

    public void removePopupButton(PopupButtonWidget popupButton) {
        sheetWidget.removePopupButton(popupButton);
    }

    public void showCellValue(String value, int col, int row, boolean formula, boolean locked) {
        // do check in case the user has changed the selected cell before the
        // formula was sent
        if (sheetWidget.getSelectedCellColumn() == col && sheetWidget.getSelectedCellRow() == row) {
            updateSelectedCellValues(col, row);
        }
    }

    public void invalidCellAddress() {
        formulaBarWidget.revertCellAddressValue();
    }

    public void focusSheet() {
        sheetWidget.focusSheet();
    }

    /**
     * @return the customEditorFactory
     */
    public SpreadsheetCustomEditorFactory getCustomEditorFactory() {
        return customEditorFactory;
    }

    /**
     * @param customEditorFactory
     *            the customEditorFactory to set
     */
    public void setCustomEditorFactory(SpreadsheetCustomEditorFactory customEditorFactory) {
        this.customEditorFactory = customEditorFactory;

        selectionHandler.newSelectedCellSet();
    }

    /**
     * Called when the {@link #customEditorFactory} might have a new editor for
     * the currently selected cell.
     */
    public void loadSelectedCellEditor() {
        if (!sheetWidget.isSelectedCellCustomized() && !cellLocked && customEditorFactory != null
                && customEditorFactory.hasCustomEditor(sheetWidget.getSelectedCellKey())) {
            Widget customEditor = customEditorFactory.getCustomEditor(sheetWidget.getSelectedCellKey());
            if (customEditor != null) {
                customCellEditorDisplayed = true;
                formulaBarWidget.setFormulaFieldEnabled(false);
                sheetWidget.displayCustomCellEditor(customEditor);
            }
        }
    }

    public void addVisibleCellComment(String key) {
        sheetWidget.setCellCommentVisible(true, key);
    }

    public void removeVisibleCellComment(String key) {
        sheetWidget.setCellCommentVisible(false, key);
    }

    /**
     * Handles overlays, currently images and charts.
     */
    void addOverlay(String key, Widget widget, OverlayInfo overlayInfo) {
        SheetOverlay overlay = new SheetOverlay(widget, overlayInfo);
        sheetWidget.addSheetOverlay(key, overlay);
    }

    void updateOverlay(String key, OverlayInfo overlayInfo) {
        sheetWidget.updateOverlayInfo(key, overlayInfo);
    }

    void removeOverlay(String key) {
        sheetWidget.removeSheetOverlay(key);
    }

    public void updateMergedRegions(final ArrayList<MergedRegion> mergedRegions) {
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {

            @Override
            public void execute() {
                // remove old, add new
                clearMergedRegions();
                if (mergedRegions != null) {
                    int i = 0;
                    while (i < mergedRegions.size()) {
                        MergedRegion newMergedRegion = mergedRegions.get(i);
                        sheetWidget.addMergedRegion(newMergedRegion);
                        i++;
                    }
                    sheetWidget.checkMergedRegionPositions();
                }

                // copy list for later
                if (mergedRegions == null) {
                    SpreadsheetWidget.this.mergedRegions = null;
                } else {
                    SpreadsheetWidget.this.mergedRegions = new ArrayList<MergedRegion>(mergedRegions);
                }
            }
        });
    }

    private void clearMergedRegions() {
        if (mergedRegions != null) {
            while (0 < mergedRegions.size()) {
                sheetWidget.removeMergedRegion(mergedRegions.remove(0), 0);
            }
        }
    }

    @Override
    public void onScrollViewChanged(int firstRowIndex, int lastRowIndex, int firstColumnIndex,
            int lastColumnIndex) {
        spreadsheetHandler.onSheetScroll(firstRowIndex, firstColumnIndex, lastRowIndex, lastColumnIndex);
        startDelayedSendingTimer();
    }

    @Override
    public void onLinkCellClick(int column, int row) {
        spreadsheetHandler.linkCellClicked(row, column);
    }

    @Override
    public void onCellRightClick(NativeEvent event, int column, int row) {
        // logic handled on server side
        if (sheetContextMenuHandler != null) {
            if (column != sheetWidget.getSelectedCellColumn() || row != sheetWidget.getSelectedCellRow()) {
                doCommitIfEditing();
            }
            sheetContextMenuHandler.cellContextMenu(event, column, row);
        }
    }

    @Override
    public void onRowHeaderRightClick(NativeEvent nativeEvent, int rowIndex) {
        if (sheetContextMenuHandler != null) {
            sheetContextMenuHandler.rowHeaderContextMenu(nativeEvent, rowIndex);
        }
    }

    @Override
    public void onColumnHeaderRightClick(NativeEvent nativeEvent, int columnIndex) {
        if (sheetContextMenuHandler != null) {
            sheetContextMenuHandler.columnHeaderContextMenu(nativeEvent, columnIndex);
        }
    }

    @Override
    public void onCellClick(int column, int row, String value, boolean shiftKey, boolean metaOrCtrlKey,
            boolean updateToActionHandler) {
        doCommitIfEditing();
        if (column == 0 || row == 0) {
            return;
        }
        boolean hasSelectedCellChangedOnClick = false;
        if (!updateToActionHandler) {
            hasSelectedCellChangedOnClick = row != sheetWidget.getSelectedCellRow()
                    || column != sheetWidget.getSelectedCellColumn();
        }
        if (shiftKey) {
            // select everything from previously selected cell to the clicked
            // cell, keep the old selected cell as the selected
            final int selectedCellCol = sheetWidget.getSelectedCellColumn();
            final int selectedCellRow = sheetWidget.getSelectedCellRow();
            int c1 = selectedCellCol > column ? column : selectedCellCol;
            int c2 = selectedCellCol > column ? selectedCellCol : column;
            int r1 = selectedCellRow > row ? row : selectedCellRow;
            int r2 = selectedCellRow > row ? selectedCellRow : row;
            MergedRegion selectedRegion = MergedRegionUtil.findIncreasingSelection(mergedRegionContainer, r1, r2,
                    c1, c2);

            if (formulaBarWidget.isEditingFormula()) {
                // do nothing here
            } else if (sheetWidget.isSelectionRangeOutlineVisible()) {
                sheetWidget.updateSelectionOutline(selectedRegion.col1, selectedRegion.col2, selectedRegion.row1,
                        selectedRegion.row2);
                sheetWidget.updateSelectedCellStyles(selectedRegion.col1, selectedRegion.col2, selectedRegion.row1,
                        selectedRegion.row2, true);
            } else {
                sheetWidget.updateSelectedCellStyles(selectedRegion.col1, selectedRegion.col2, selectedRegion.row1,
                        selectedRegion.row2, false);
            }
            // TODO update the selection coherence, if the areas align properly
            if (!sheetWidget.isCoherentSelection()) {
            }

            if (formulaBarWidget.isEditingFormula()) {

                formulaBarWidget.setFormulaCellRange(tempSelectionStartCol, tempSelectionStartRow, column, row);

            } else if (updateToActionHandler) {
                if (sheetWidget.isSelectionRangeOutlineVisible()) {
                    spreadsheetHandler.cellRangeSelected(selectedRegion.row1, selectedRegion.col1,
                            selectedRegion.row2, selectedRegion.col2);
                } else {
                    spreadsheetHandler.cellsAddedToRangeSelection(selectedRegion.row1, selectedRegion.col1,
                            selectedRegion.row2, selectedRegion.col2);
                }
                startDelayedSendingTimer();
            }
        } else if (metaOrCtrlKey) {
            // add the selected cell into the selection, set it as the selected
            if (column == sheetWidget.getSelectedCellColumn() && row == sheetWidget.getSelectedCellRow()) {
                // clicked on the selected cell again -> nothing happens
                return;
            }

            if (formulaBarWidget.isEditingFormula()) {
                formulaBarWidget.addFormulaCellRange(column, row, column, row);
            } else {

                // TODO update the selection coherence, if the areas align
                // properly
                if (sheetWidget.isCoherentSelection()) {
                    sheetWidget.setCoherentSelection(false);
                }
                if (sheetWidget.isSelectionRangeOutlineVisible()) {
                    sheetWidget.setSelectionRangeOutlineVisible(false);
                }
                sheetWidget.swapCellSelection(column, row);
                selectionHandler.newSelectedCellSet();
                if (hasSelectedCellChangedOnClick) {
                    updateSelectedCellValues(column, row);
                }
                if (updateToActionHandler) {
                    spreadsheetHandler.cellAddedToSelectionAndSelected(row, column);
                    startDelayedSendingTimer();
                }
            }
        } else {
            // select cell
            MergedRegion cell = mergedRegionContainer.getMergedRegionStartingFrom(column, row);

            if (formulaBarWidget.isEditingFormula()) {

                tempSelectionStartCol = column;
                tempSelectionStartRow = row;

                formulaBarWidget.setFormulaCellRange(column, row, column, row);
            } else {

                if (!sheetWidget.isCoherentSelection()) {
                    sheetWidget.setCoherentSelection(true);
                }
                if (!sheetWidget.isSelectionRangeOutlineVisible()) {
                    sheetWidget.setSelectionRangeOutlineVisible(true);
                    sheetWidget.clearSelectedCellStyle();
                }
                sheetWidget.setSelectedCell(column, row);
                if (cell != null) {
                    sheetWidget.updateSelectionOutline(cell.col1, cell.col2, cell.row1, cell.row2);
                    sheetWidget.updateSelectedCellStyles(cell.col1, cell.col2, cell.row1, cell.row2, true);

                    selectionHandler.setColBeforeMergedCell(cell.col1);
                    selectionHandler.setRowBeforeMergedCell(cell.row1);
                } else {
                    sheetWidget.updateSelectionOutline(column, column, row, row);
                    sheetWidget.updateSelectedCellStyles(column, column, row, row, true);
                }
                if (hasSelectedCellChangedOnClick) {
                    updateSelectedCellValues(column, row);
                }
                if (updateToActionHandler) {
                    selectionHandler.newSelectedCellSet();
                    spreadsheetHandler.cellSelected(row, column, true);
                    startDelayedSendingTimer();
                }
            }
        }
    }

    public void updateSelectedCellValues(int column, int row) {
        if (!sheetWidget.isEditingCell()) {
            String formulaValue = sheetWidget.getCellFormulaValue(column, row);
            if (formulaValue != null && !formulaValue.isEmpty()) {
                formulaBarWidget.setCellFormulaValue(formulaValue);
                sheetWidget.updateInputValue("=" + formulaValue);
            } else {
                formulaBarWidget.setCellPlainValue(sheetWidget.getOriginalCellValue(column, row));
            }
        }
        cellLocked = sheetWidget.isCellLocked(column, row);
        if (!customCellEditorDisplayed) {
            formulaBarWidget.setFormulaFieldEnabled(!cellLocked);
        } else {
            sheetWidget
                    .displayCustomCellEditor(customEditorFactory.getCustomEditor(sheetWidget.getSelectedCellKey()));
        }
        formulaBarWidget.setSelectedCellAddress(createCellAddress(column, row));
    }

    @Override
    public void onRowHeaderClick(int row, boolean shiftPressed, boolean metaOrCrtlPressed) {
        int firstColumnIndex = sheetWidget.hasFrozenColumns() ? 1 : sheetWidget.getLeftVisibleColumnIndex();
        doCommitIfEditing();
        if (!shiftPressed) {
            updateSelectedCellValues(firstColumnIndex, row);
        }
        if (shiftPressed) {
            // keep selected, add the whole range from old selected as range
            // select all rows from previous to new
            int c1 = 1;
            int c2 = cols;
            final int selectedCellRow = sheetWidget.getSelectedCellRow();
            int r1 = selectedCellRow > row ? row : selectedCellRow;
            int r2 = selectedCellRow > row ? selectedCellRow : row;
            if (sheetWidget.isSelectionRangeOutlineVisible()) {
                sheetWidget.updateSelectionOutline(c1, c2, r1, r2);
                sheetWidget.updateSelectedCellStyles(c1, c2, r1, r2, true);
            } else {
                sheetWidget.updateSelectedCellStyles(c1, c2, r1, r2, false);
            }
            // TODO update the selection coherence, if the areas align properly
            if (!sheetWidget.isCoherentSelection()) {
            }
            if (sheetWidget.isSelectionRangeOutlineVisible()) {
                spreadsheetHandler.cellRangeSelected(r1, c1, r2, c2);
            } else {
                spreadsheetHandler.cellsAddedToRangeSelection(r1, c1, r2, c2);
            }
        } else if (metaOrCrtlPressed) {
            // change selected, add whole row to range
            // TODO update the selection coherence, if the areas align properly
            if (sheetWidget.isCoherentSelection()) {
                sheetWidget.setCoherentSelection(false);
            }
            if (sheetWidget.isSelectionRangeOutlineVisible()) {
                sheetWidget.setSelectionRangeOutlineVisible(false);
            }
            // set the row first cell as the selected
            sheetWidget.swapCellSelection(firstColumnIndex, row);
            selectionHandler.newSelectedCellSet();
            // add the selection styles
            sheetWidget.updateSelectedCellStyles(1, cols, row, row, false);
            spreadsheetHandler.rowAddedToRangeSelection(row, firstColumnIndex);
        } else {
            if (!sheetWidget.isCoherentSelection()) {
                sheetWidget.setCoherentSelection(true);
            }
            if (!sheetWidget.isSelectionRangeOutlineVisible()) {
                sheetWidget.setSelectionRangeOutlineVisible(true);
                sheetWidget.clearSelectedCellStyle();
            }

            sheetWidget.setSelectedCell(firstColumnIndex, row);
            sheetWidget.updateSelectionOutline(1, cols, row, row);
            sheetWidget.updateSelectedCellStyles(1, cols, row, row, true);
            selectionHandler.newSelectedCellSet();
            spreadsheetHandler.rowSelected(row, firstColumnIndex);
        }
        startDelayedSendingTimer();
    }

    @Override
    public void onColumnHeaderClick(int column, boolean shiftPressed, boolean metaOrCrtlPressed) {
        doCommitIfEditing();
        int firstRowIndex = sheetWidget.hasFrozenRows() ? 1 : sheetWidget.getTopVisibleRowIndex();
        if (!shiftPressed) {
            updateSelectedCellValues(column, firstRowIndex);
        }
        if (shiftPressed) {
            // keep selected, add the whole range from old selected as range
            // select or columns from previous to new
            int selectedCellCol = sheetWidget.getSelectedCellColumn();
            int c1 = selectedCellCol > column ? column : selectedCellCol;
            int c2 = selectedCellCol > column ? selectedCellCol : column;
            int r1 = 1;
            int r2 = rows;
            if (sheetWidget.isSelectionRangeOutlineVisible()) {
                sheetWidget.updateSelectionOutline(c1, c2, r1, r2);
                sheetWidget.updateSelectedCellStyles(c1, c2, r1, r2, true);
            } else {
                sheetWidget.updateSelectedCellStyles(c1, c2, r1, r2, false);
            }
            // TODO update the selection coherence, if the areas align properly
            // if (!sheetWidget.isCoherentSelection()) {
            // }
            if (sheetWidget.isSelectionRangeOutlineVisible()) {
                spreadsheetHandler.cellRangeSelected(r1, c1, r2, c2);
            } else {
                spreadsheetHandler.cellsAddedToRangeSelection(r1, c1, r2, c2);
            }
        } else if (metaOrCrtlPressed) {
            // change selected, add whole row to range
            // TODO update the selection coherence, if the areas align properly
            if (sheetWidget.isCoherentSelection()) {
                sheetWidget.setCoherentSelection(false);
            }
            if (sheetWidget.isSelectionRangeOutlineVisible()) {
                sheetWidget.setSelectionRangeOutlineVisible(false);
            }
            // set the row first cell as the selected
            sheetWidget.swapCellSelection(column, firstRowIndex);
            selectionHandler.newSelectedCellSet();
            // add the selection styles
            sheetWidget.updateSelectedCellStyles(column, column, 1, rows, false);
            spreadsheetHandler.columnAddedToSelection(firstRowIndex, column);
        } else {
            if (!sheetWidget.isCoherentSelection()) {
                sheetWidget.setCoherentSelection(true);
            }
            if (!sheetWidget.isSelectionRangeOutlineVisible()) {
                sheetWidget.setSelectionRangeOutlineVisible(true);
                sheetWidget.clearSelectedCellStyle();
            }
            sheetWidget.setSelectedCell(column, firstRowIndex);
            sheetWidget.updateSelectionOutline(column, column, 1, rows);
            sheetWidget.updateSelectedCellStyles(column, column, 1, rows, true);
            selectionHandler.newSelectedCellSet();
            spreadsheetHandler.columnSelected(column, firstRowIndex);
        }
        startDelayedSendingTimer();
    }

    @Override
    public void onColumnHeaderResizeDoubleClick(int columnIndex) {
        spreadsheetHandler.onColumnAutofit(columnIndex);
    }

    void doCommitIfEditing() {

        if (formulaBarWidget.isEditingFormula()) {
            // do nothing
        } else if (inlineEditing || formulaBarEditing) {
            cancelDeferredCommit = true;
            final String editedValue = formulaBarWidget.getFormulaFieldValue();
            spreadsheetHandler.cellValueEdited(sheetWidget.getSelectedCellRow(),
                    sheetWidget.getSelectedCellColumn(), editedValue);
            cellEditingDone(editedValue, true);
        } else if (customCellEditorDisplayed) {
            customCellEditorDisplayed = false;
            sheetWidget.removeCustomCellEditor();
            formulaBarWidget.setFormulaFieldEnabled(true);
        }
    }

    @Override
    public void onSelectingCellsWithDrag(int col, int row) {
        if (col == 0) {
            col = 1;
        } else if (col < 0) {
            col = sheetWidget.getRightVisibleColumnIndex() + 1;
        }
        if (col > cols) {
            col = cols;
        }
        if (row == 0) {
            row = 1;
        } else if (row < 0) {
            row = sheetWidget.getBottomVisibleRowIndex() + 1;
        }
        if (row > rows) {
            row = rows;
        }
        int selectedCellColumn = sheetWidget.getSelectedCellColumn();
        int selectedCellRow = sheetWidget.getSelectedCellRow();
        int col1;
        int col2;
        int row1;
        int row2;
        if (col <= selectedCellColumn) {
            col1 = col;
            col2 = selectedCellColumn;
        } else {
            col1 = selectedCellColumn;
            col2 = col;
        }
        if (row <= selectedCellRow) {
            row1 = row;
            row2 = selectedCellRow;
        } else {
            row1 = selectedCellRow;
            row2 = row;
        }

        if (formulaBarWidget.isEditingFormula()) {

            formulaBarWidget.setFormulaCellRange(tempSelectionStartCol, tempSelectionStartRow, col, row);
        } else {
            MergedRegion selectedRegion = MergedRegionUtil.findIncreasingSelection(mergedRegionContainer, row1,
                    row2, col1, col2);
            sheetWidget.updateSelectionOutline(selectedRegion.col1, selectedRegion.col2, selectedRegion.row1,
                    selectedRegion.row2);
            sheetWidget.updateSelectedCellStyles(selectedRegion.col1, selectedRegion.col2, selectedRegion.row1,
                    selectedRegion.row2, true);

            formulaBarWidget.setSelectedCellAddress(createRangeSelectionString(selectedRegion.col1,
                    selectedRegion.col2, selectedRegion.row1, selectedRegion.row2));
        }
    }

    @Override
    public void onFinishedSelectingCellsWithDrag(int col1, int col2, int row1, int row2) {
        if (col1 == 0 || col2 == 0 || row1 == 0 || row2 == 0 || col1 == col2 && row1 == row2
                && col1 == sheetWidget.getSelectedCellColumn() && row1 == sheetWidget.getSelectedCellRow()) {
            return;
        }

        int origCol2 = col2;
        int origRow2 = row2;

        // swap coordinates so that 1 is smaller than 2
        int temp;
        if (col1 > col2) {
            temp = col1;
            col1 = col2;
            col2 = temp;
        }
        if (row1 > row2) {
            temp = row1;
            row1 = row2;
            row2 = temp;
        }

        if (formulaBarWidget.isEditingFormula()) {
            formulaBarWidget.setFormulaCellRange(tempSelectionStartCol, tempSelectionStartRow, origCol2, origRow2);
            formulaBarWidget.clearFormulaSelection();
        } else {

            MergedRegion selectedRegion = MergedRegionUtil.findIncreasingSelection(mergedRegionContainer, row1,
                    row2, col1, col2);
            spreadsheetHandler.cellRangePainted(sheetWidget.getSelectedCellRow(),
                    sheetWidget.getSelectedCellColumn(), selectedRegion.row1, selectedRegion.col1,
                    selectedRegion.row2, selectedRegion.col2);
            formulaBarWidget.setSelectedCellAddress(
                    createCellAddress(sheetWidget.getSelectedCellColumn(), sheetWidget.getSelectedCellRow()));
            selectionHandler.newSelectedCellSet();
            startDelayedSendingTimer();
        }
    }

    @Override
    public void onCellDoubleClick(int column, int row, String value) {
        if (sheetWidget.getSelectedCellRow() != row && sheetWidget.getSelectedCellColumn() != column) {
            onCellClick(column, row, value, false, false, true);
        } else {
            Cell cell = sheetWidget.getCell(column, row);
            value = cell.getValue();
            cachedCellValue = value;
            formulaBarWidget.cacheFormulaFieldValue();
            value = formulaBarWidget.getFormulaFieldValue();
        }
        formulaBarEditing = false;
        checkEditableAndNotify();
        if (!cellLocked) {
            if (!inlineEditing && !customCellEditorDisplayed) {
                inlineEditing = true;
                sheetWidget.startEditingCell(true, true, value);
                formulaBarWidget.startInlineEdit(true);
            }
        }
    }

    @Override
    public void onCellInputBlur(final String inputValue) {
        // need to do this deferred in case focus moved to the formula field
        if (inlineEditing && !formulaBarWidget.isEditingFormula()) {
            doDeferredCellValueCommit(inputValue, true);
        }
    }

    /* This is only for when focus is changed from formula field to inline input */
    @Override
    public void onCellInputFocus() {
        if (!inlineEditing && !formulaBarWidget.isEditingFormula()) {
            inlineEditing = true;
            cancelDeferredCommit = true;
            if (formulaBarEditing) { // just swap, everything should work
                formulaBarEditing = false;
            } else { // need to make sure the input value is correct
                sheetWidget.startEditingCell(true, false, formulaBarWidget.getFormulaFieldValue());
                formulaBarWidget.startInlineEdit(true);
            }
        }
    }

    @Override
    public void onCellInputCancel() {
        cellEditingDone(cachedCellValue, true);
        formulaBarWidget.revertCellValue();
        sheetWidget.focusSheet();
    }

    @Override
    public void onCellInputEnter(String value, boolean shift) {
        spreadsheetHandler.cellValueEdited(sheetWidget.getSelectedCellRow(), sheetWidget.getSelectedCellColumn(),
                value);
        cellEditingDone(value, true);
        sheetWidget.focusSheet();
        if (shift) {
            selectionHandler.moveSelectionUp(false);
        } else {
            selectionHandler.moveSelectionDown(false);
        }
    }

    @Override
    public void onCellInputTab(String value, boolean shift) {
        spreadsheetHandler.cellValueEdited(sheetWidget.getSelectedCellRow(), sheetWidget.getSelectedCellColumn(),
                value);
        cellEditingDone(value, true);
        sheetWidget.focusSheet();
        if (shift) {
            selectionHandler.moveSelectionLeft(false);
        } else {
            selectionHandler.moveSelectionRight(false);
        }
    }

    @Override
    public void onCellInputValueChange(String value) {
        formulaBarWidget.setFormulaFieldValue(value);
    }

    @Override
    public void onSheetKeyPress(NativeEvent event, String enteredCharacter) {
        // Here we need to also check for char code 13 (which is code for enter)
        // since for some reason the enter key is reported having both
        // KeyCodes.ENTER and the char code 13, whereas other such non-character
        // keys here have no char codes. Enter key must be detected here to
        // start editing a cell.
        if ((event.getCharCode() == 0 && event.getKeyCode() != KeyCodes.KEY_SPACE) || event.getCharCode() == 13) {
            switch (event.getKeyCode()) {
            case KeyCodes.KEY_BACKSPACE:
            case KeyCodes.KEY_DELETE:
                checkEditableAndNotify();
                if (!cellLocked) {
                    spreadsheetHandler.deleteSelectedCells();
                    formulaBarWidget.setCellPlainValue("");
                }
                break;
            case KeyCodes.KEY_DOWN:
                if (event.getShiftKey()) {
                    selectionHandler.increaseVerticalSelection(true);
                } else {
                    selectionHandler.moveSelectionDown(true);
                }
                break;
            case KeyCodes.KEY_LEFT:
                if (event.getShiftKey()) {
                    selectionHandler.increaseHorizontalSelection(false);
                } else {
                    selectionHandler.moveSelectionLeft(true);
                }
                break;
            case KeyCodes.KEY_TAB:
                if (event.getShiftKey()) {
                    selectionHandler.moveSelectionLeft(isSelectedCellHidden());
                } else {
                    selectionHandler.moveSelectionRight(isSelectedCellHidden());
                }
                break;
            case KeyCodes.KEY_RIGHT:
                if (event.getShiftKey()) {
                    selectionHandler.increaseHorizontalSelection(true);
                } else {
                    selectionHandler.moveSelectionRight(true);
                }
                break;
            case KeyCodes.KEY_UP:
                if (event.getShiftKey()) {
                    selectionHandler.increaseVerticalSelection(false);
                } else {
                    selectionHandler.moveSelectionUp(true);
                }
                break;
            case KeyCodes.KEY_ALT:
            case KeyCodes.KEY_CTRL:
            case KeyCodes.KEY_END:
            case KeyCodes.KEY_ESCAPE:
            case KeyCodes.KEY_HOME:
            case KeyCodes.KEY_PAGEDOWN:
            case KeyCodes.KEY_PAGEUP:
            case KeyCodes.KEY_SHIFT:
                break;
            case KeyCodes.KEY_F2:
            case KeyCodes.KEY_ENTER:
                if (KeyCodes.KEY_ENTER == event.getKeyCode()) {
                    if (isSelectedCellHidden()) {
                        selectionHandler.moveSelectionDown(true);
                        break;
                    } else {
                        if (sheetWidget.getSelectionLeftCol() != sheetWidget.getSelectionRightCol()
                                || sheetWidget.getSelectionTopRow() != sheetWidget.getSelectionBottomRow()) {
                            if (event.getShiftKey()) {
                                selectionHandler.moveSelectionUp(false);
                            } else {
                                selectionHandler.moveSelectionDown(false);
                            }
                            break;
                        }
                    }
                }
                checkEditableAndNotify();
                if (!sheetWidget.isSelectedCellCustomized() && !inlineEditing && !cellLocked
                        && !customCellEditorDisplayed) {
                    cachedCellValue = sheetWidget.getSelectedCellLatestValue();
                    formulaBarWidget.cacheFormulaFieldValue();
                    formulaBarEditing = false;
                    inlineEditing = true;
                    sheetWidget.startEditingCell(true, true, formulaBarWidget.getFormulaFieldValue());
                    formulaBarWidget.startInlineEdit(true);
                }
                break;
            }
        } else {
            if (!isSelectedCellHidden()) {
                checkEditableAndNotify();

                if (!sheetWidget.isSelectedCellCustomized() && !inlineEditing && !cellLocked
                        && !customCellEditorDisplayed) {
                    // cache value and start editing cell as empty
                    inlineEditing = true;
                    cachedCellValue = sheetWidget.getSelectedCellLatestValue();

                    formulaBarWidget.startInlineEdit(true);

                    if (cachedCellValue.endsWith("%") || sheetWidget.isSelectedCellPergentage()) {

                        if (isNumericChar(enteredCharacter)) {
                            enteredCharacter = enteredCharacter + "%";
                        }
                        sheetWidget.startEditingCell(true, true, enteredCharacter);
                    } else {
                        sheetWidget.startEditingCell(true, true, enteredCharacter);
                        formulaBarWidget.cacheFormulaFieldValue();
                    }
                    formulaBarWidget.setCellPlainValue(enteredCharacter);
                }
            }
        }
    }

    private static boolean isNumericChar(String input) {
        Set<String> allowedChars = new HashSet<String>();
        allowedChars.add("0");
        allowedChars.add("1");
        allowedChars.add("2");
        allowedChars.add("3");
        allowedChars.add("4");
        allowedChars.add("5");
        allowedChars.add("6");
        allowedChars.add("7");
        allowedChars.add("8");
        allowedChars.add("9");
        allowedChars.add("-");
        allowedChars.add("+");

        return allowedChars.contains(input);
    }

    private boolean isSelectedCellHidden() {
        return hiddenColumnIndexes.contains(sheetWidget.getSelectedCellColumn())
                || hiddenRowIndexes.contains(sheetWidget.getSelectedCellRow());
    }

    /**
     * Checks if selected cell is locked, and sends an RPC to server if it is.
     */
    private void checkEditableAndNotify() {
        if (cellLocked) {

            if (!okToSendCellProtectRpc) {
                // don't send just yet
                return;
            }

            Timer timer = new Timer() {
                @Override
                public void run() {
                    okToSendCellProtectRpc = true;
                }
            };
            timer.schedule(1000);

            okToSendCellProtectRpc = false;

            ServerConnector connector = Util.findConnectorFor(this);
            SpreadsheetServerRpc rpc = RpcProxy.create(SpreadsheetServerRpc.class, connector);

            rpc.protectedCellWriteAttempted();
        }
    }

    @Override
    public void onAddressEntered(String value) {
        spreadsheetHandler.sheetAddressChanged(value);
    }

    @Override
    public void onAddressFieldEsc() {
        sheetWidget.focusSheet();
    }

    @Override
    public void onSheetTabSelected(int sheetIndex) {
        if (formulaBarWidget.isEditingFormula()) {
            // TODO commit or ignore value? this ignores. Excel remembers that
            // editor was open. If editing from formula bar, value is stored..
            formulaBarWidget.stopInlineEdit();
            sheetWidget.stopEditingCell(false);
        }

        int scrollLeft = sheetWidget.getSheetScrollLeft();
        int scrollTop = sheetWidget.getSheetScrollTop();
        spreadsheetHandler.sheetSelected(sheetIndex, scrollLeft, scrollTop);
    }

    @Override
    public void onFirstTabIndexChange(int firstVisibleTab) {
        // Disabled because not working in Apache POI
        // actionHandler.firstVisibleTabChanged(firstVisibleTab);
    }

    @Override
    public void onSheetRename(int sheetIndex, String newName) {
        spreadsheetHandler.sheetRenamed(sheetIndex, newName);
    }

    @Override
    public void onNewSheetCreated() {
        int scrollLeft = sheetWidget.getSheetScrollLeft();
        int scrollTop = sheetWidget.getSheetScrollTop();
        spreadsheetHandler.sheetCreated(scrollLeft, scrollTop);
    }

    @Override
    public void onSheetRenameCancel() {
        sheetWidget.focusSheet();
    }

    @Override
    public void onSheetTabSheetFocus() {
        sheetWidget.focusSheet(false);
    }

    @Override
    public int[] getRowHeightsPX() {
        return sheetWidget.getRowHeights();
    }

    @Override
    public MergedRegion getMergedRegion(int column, int row) {
        return mergedRegionContainer.getMergedRegion(column, row);
    }

    @Override
    public MergedRegion getMergedRegionStartingFrom(int column, int row) {
        return mergedRegionContainer.getMergedRegionStartingFrom(column, row);
    }

    @Override
    public void onSelectionIncreasePainted(int c1, int c2, int r1, int r2) {
        MergedRegion evenedRegion = MergedRegionUtil.findIncreasingSelection(mergedRegionContainer, r1, r2, c1, c2);
        // discard painted area if merged cells don't align
        if (evenedRegion.col1 == c1 && evenedRegion.col2 == c2 && evenedRegion.row1 == r1
                && evenedRegion.row2 == r2) {
            spreadsheetHandler.selectionIncreasePainted(r1, c1, r2, c2);
            startDelayedSendingTimer();
        }
    }

    @Override
    public void onSelectionDecreasePainted(int colEdgeIndex, int rowEdgeIndex) {
        // the selection widget has made sure the decreasing area is not in
        // middle of merged cells.
        spreadsheetHandler.selectionDecreasePainted(rowEdgeIndex, colEdgeIndex);
        startDelayedSendingTimer();
    }

    @Override
    public void onFormulaFieldFocus(String value) {
        formulaBarEditing = true;
        cancelDeferredCommit = true;
        if (inlineEditing) { // just swap and everything should work
            inlineEditing = false;
        } else {
            if (sheetWidget.isSelectedCellCustomized()) {
                cachedCellValue = "";
            } else {
                cachedCellValue = sheetWidget.getSelectedCellLatestValue();
            }
        }
    }

    @Override
    public void onFormulaFieldBlur(final String value) {
        // need to do this as deferred because in case the focus was passed to
        // inline input element
        if (formulaBarEditing) {
            doDeferredCellValueCommit(value, false);
        }
    }

    @Override
    public void onFormulaEnter(String value) {
        spreadsheetHandler.cellValueEdited(sheetWidget.getSelectedCellRow(), sheetWidget.getSelectedCellColumn(),
                value);
        cellEditingDone(value, true);
        sheetWidget.focusSheet();
        selectionHandler.moveSelectionDown(false);
    }

    @Override
    public void onFormulaTab(String value, boolean focusSheet) {
        spreadsheetHandler.cellValueEdited(sheetWidget.getSelectedCellRow(), sheetWidget.getSelectedCellColumn(),
                value);
        cellEditingDone(value, focusSheet);
        if (focusSheet) {
            sheetWidget.focusSheet();
            selectionHandler.moveSelectionRight(false);
        }
    }

    @Override
    public void onFormulaEsc() {
        cellEditingDone(cachedCellValue, true);
        sheetWidget.focusSheet();
    }

    @Override
    public void onFormulaValueChange(String value) {
        if (!sheetWidget.isSelectedCellCustomized()) {
            sheetWidget.updateInputValue(value);
        }
    }

    @Override
    public void onRowsResized(Map<Integer, Float> newSizes) {
        for (Entry<Integer, Float> entry : newSizes.entrySet()) {
            int index = entry.getKey();
            float size = entry.getValue();
            if (size == 0.0F) {
                if (hiddenRowIndexes == null) {
                    hiddenRowIndexes = new ArrayList<Integer>();
                    hiddenRowIndexes.add(index);
                } else if (!hiddenRowIndexes.contains(index)) {
                    hiddenRowIndexes.add(index);
                }
            }
            rowH[index - 1] = size;
        }
        sheetWidget.relayoutSheet(false);
        if (mergedRegions != null) {
            for (MergedRegion region : mergedRegions) {
                sheetWidget.updateMergedRegionSize(region);
            }
        }
        cancelNextSheetRelayout = true;
        int[] x = sheetWidget.getSheetDisplayRange();
        spreadsheetHandler.rowsResized(newSizes, x[0], x[1], x[2], x[3]);
    }

    @Override
    public void onColumnsResized(Map<Integer, Integer> newSizes) {
        for (Entry<Integer, Integer> entry : newSizes.entrySet()) {
            int index = entry.getKey();
            int size = entry.getValue();
            if (size == 0F) {
                if (hiddenColumnIndexes == null) {
                    hiddenColumnIndexes = new ArrayList<Integer>();
                    hiddenColumnIndexes.add(index);
                } else if (!hiddenColumnIndexes.contains(index)) {
                    hiddenColumnIndexes.add(index);
                }
            }
            colW[index - 1] = size;
        }
        sheetWidget.relayoutSheet(false);
        if (mergedRegions != null) {
            for (MergedRegion region : mergedRegions) {
                sheetWidget.updateMergedRegionSize(region);
            }
        }
        cancelNextSheetRelayout = true;
        int[] x = sheetWidget.getSheetDisplayRange();
        spreadsheetHandler.columnResized(newSizes, x[0], x[1], x[2], x[3]);
    }

    @Override
    public void onRedoPress() {
        spreadsheetHandler.onRedo();
    }

    @Override
    public void onUndoPress() {
        spreadsheetHandler.onUndo();
    }

    /**
     * update the sheet display after editing has finished
     *
     * @param focusSheet
     *
     * @param focusSheet
     */
    private void cellEditingDone(String value, boolean focusSheet) {
        inlineEditing = false;
        formulaBarWidget.stopInlineEdit();
        formulaBarEditing = false;
        if (!sheetWidget.isSelectedCellCustomized()) {
            if (value == null) {
                value = "";
            }
            selectedCellIsFormulaType = value.startsWith("=") || value.startsWith("+");
            sheetWidget.stopEditingCell(focusSheet);

            if (!selectedCellIsFormulaType) {
                sheetWidget.updateSelectedCellValue(value);
            }
        }
    }

    /**
     *
     * @param value
     * @param focusSheet
     */
    private void doDeferredCellValueCommit(final String value, final boolean focusSheet) {
        cancelDeferredCommit = false;
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {

            @Override
            public void execute() {
                if (!cancelDeferredCommit) {
                    spreadsheetHandler.cellValueEdited(sheetWidget.getSelectedCellRow(),
                            sheetWidget.getSelectedCellColumn(), value);
                    cellEditingDone(value, focusSheet);
                }
            }
        });
    }

    protected String createRangeSelectionString(int col1, int col2, int row1, int row2) {
        final StringBuffer sb = new StringBuffer();
        sb.append(Math.abs(row2 - row1) + 1);
        sb.append("R");
        sb.append(" x ");
        sb.append(Math.abs(col2 - col1) + 1);
        sb.append("C");
        return sb.toString();
    }

    @Override
    public String createCellAddress(int column, int row) {
        final String c = column > 0 ? getColHeader(column) : "";
        final String r = row > 0 ? Integer.toString(row) : "";
        return c + r;
    }

    public void setRowBufferSize(int rowBufferSize) {
        this.rowBufferSize = rowBufferSize;
    }

    public void setColumnBufferSize(int columnBufferSize) {
        this.columnBufferSize = columnBufferSize;
    }

    public void setRows(int rows) {
        this.rows = rows;
    }

    public void setCols(int cols) {
        this.cols = cols;
    }

    public void setColGroupingData(List<GroupingData> data) {
        sheetWidget.setColGroupingData(data);
    }

    public void setRowGroupingData(List<GroupingData> data) {
        sheetWidget.setRowGroupingData(data);
    }

    public void setRowH(float[] rowH) {
        this.rowH = rowH;
    }

    public void setColW(int[] colW) {
        this.colW = colW;
    }

    public void setDefRowH(float defRowH) {
        this.defRowH = defRowH;
    }

    public void setDefColW(int defColW) {
        this.defColW = defColW;
    }

    public void setVerticalScrollPositions(int[] verticalScrollPositions) {
        this.verticalScrollPositions = verticalScrollPositions;
    }

    public void setHorizontalScrollPositions(int[] horizontalScrollPositions) {
        this.horizontalScrollPositions = horizontalScrollPositions;
    }

    public void setVerticalSplitPosition(int verticalSplitPosition) {
        sheetWidget.setVerticalSplitPosition(verticalSplitPosition);
    }

    public void setHorizontalSplitPosition(int horizontalSplitPosition) {
        sheetWidget.setHorizontalSplitPosition(horizontalSplitPosition);
    }

    public void setCellStyleToCSSStyle(HashMap<Integer, String> cellStyleToCSSStyle) {
        if (this.cellStyleToCSSStyle == null) {
            this.cellStyleToCSSStyle = cellStyleToCSSStyle;
        } else {
            this.cellStyleToCSSStyle.clear();
            if (cellStyleToCSSStyle != null) {
                this.cellStyleToCSSStyle.putAll(cellStyleToCSSStyle);
            }
        }
    }

    public void setRowIndexToStyleIndex(HashMap<Integer, Integer> rowIndexToStyleIndex) {
        if (this.rowIndexToStyleIndex == null) {
            this.rowIndexToStyleIndex = rowIndexToStyleIndex;
        } else {
            this.rowIndexToStyleIndex.clear();
            if (rowIndexToStyleIndex != null) {
                this.rowIndexToStyleIndex.putAll(rowIndexToStyleIndex);
            }
        }
    }

    public void setColumnIndexToStyleIndex(HashMap<Integer, Integer> columnIndexToStyleIndex) {
        if (this.columnIndexToStyleIndex == null) {
            this.columnIndexToStyleIndex = columnIndexToStyleIndex;
        } else {
            this.columnIndexToStyleIndex.clear();
            if (columnIndexToStyleIndex != null) {
                this.columnIndexToStyleIndex.putAll(columnIndexToStyleIndex);
            }
        }
    }

    public void setLockedColumnIndexes(Set<Integer> lockedColumnIndexes) {
        if (this.lockedColumnIndexes == null) {
            this.lockedColumnIndexes = lockedColumnIndexes;
        } else {
            this.lockedColumnIndexes.clear();
            if (lockedColumnIndexes != null) {
                this.lockedColumnIndexes.addAll(lockedColumnIndexes);
            }
        }
    }

    public void setLockedRowIndexes(Set<Integer> lockedRowIndexes) {
        if (this.lockedRowIndexes == null) {
            this.lockedRowIndexes = lockedRowIndexes;
        } else {
            this.lockedRowIndexes.clear();
            if (lockedRowIndexes != null) {
                this.lockedRowIndexes.addAll(lockedRowIndexes);
            }
        }
    }

    public void setShiftedCellBorderStyles(ArrayList<String> shiftedCellBorderStyles) {
        sheetWidget.removeShiftedCellBorderStyles();
        if (shiftedCellBorderStyles != null) {
            sheetWidget.addShiftedCellBorderStyles(shiftedCellBorderStyles);
        }
    }

    public void setHyperlinksTooltips(HashMap<String, String> cellLinksMap) {
        sheetWidget.setCellLinks(cellLinksMap);
    }

    public void setSheetProtected(boolean sheetProtected) {
        if (this.sheetProtected != sheetProtected) {
            this.sheetProtected = sheetProtected;
            if (sheetProtected) {
                addStyleName("protected");
            } else {
                removeStyleName("protected");
            }
            if (loaded) {
                if (sheetProtected) {
                    if (customCellEditorDisplayed) {
                        customCellEditorDisplayed = false;
                        sheetWidget.removeCustomCellEditor();
                    }
                } else { // might need to load the custom editor
                    cellLocked = false;
                    selectionHandler.newSelectedCellSet();
                    if (customCellEditorDisplayed) {
                        // need to update the editor value on client side
                        spreadsheetHandler.cellSelected(sheetWidget.getSelectedCellRow(),
                                sheetWidget.getSelectedCellColumn(), false);
                        startDelayedSendingTimer();
                    }
                }
            }
        }
    }

    @Override
    public boolean isSheetProtected() {
        return sheetProtected;
    }

    @Override
    public boolean isColProtected(int col) {
        return lockedColumnIndexes.contains(col);
    }

    @Override
    public boolean isRowProtected(int row) {
        return lockedRowIndexes.contains(row);
    }

    public void setWorkbookProtected(boolean workbookProtected) {
        sheetTabSheet.setReadOnly(workbookProtected);
    }

    public void setHiddenColumnIndexes(ArrayList<Integer> hiddenColumnIndexes) {
        this.hiddenColumnIndexes = new ArrayList<Integer>(hiddenColumnIndexes);
    }

    public void setHiddenRowIndexes(ArrayList<Integer> hiddenRowIndexes) {
        this.hiddenRowIndexes = new ArrayList<Integer>(hiddenRowIndexes);
    }

    public void setCellComments(HashMap<String, String> cellComments, HashMap<String, String> cellCommentAuthors) {
        sheetWidget.setCellComments(cellComments, cellCommentAuthors);
    }

    public void setInvalidFormulaCells(Set<String> invalidFormulaCells) {
        sheetWidget.setInvalidFormulaCells(invalidFormulaCells);
    }

    public void setInvalidFormulaErrorMessage(String invalidFormulaMessage) {
        sheetWidget.setInvalidFormulaMessage(invalidFormulaMessage);
    }

    @Override
    public Map<Integer, String> getCellStyleToCSSStyle() {
        return cellStyleToCSSStyle;
    }

    @Override
    public Map<Integer, Integer> getRowIndexToStyleIndex() {
        return rowIndexToStyleIndex;
    }

    @Override
    public Map<Integer, Integer> getColumnIndexToStyleIndex() {
        return columnIndexToStyleIndex;
    }

    @Override
    public float getRowHeight(int row) {
        // doesn't take hidden rows into account! (but height is 0 for those)
        if (rowH.length >= row) {
            return rowH[row - 1];
        } else {
            return defRowH;
        }
    }

    @Override
    public int getColWidth(int col) {
        // doesn't take hidden columns into account! (but width is 0 for those)
        if (col > 0 && colW.length >= col) {
            return colW[col - 1];
        } else {
            return defColW;
        }
    }

    @Override
    public int getColWidthActual(int col) {
        if (hiddenColumnIndexes != null && hiddenColumnIndexes.contains(col)) {
            return 0;
        } else {
            return getColWidth(col);
        }
    }

    /** Get column header for column indexed 1.. */
    @Override
    public final String getColHeader(int col) {
        String h = "";
        while (col > 0) {
            h = (char) ('A' + (col - 1) % 26) + h;
            col = (col - 1) / 26;
        }
        return h;
    }

    /** Get row header for rows indexed 1.. */
    @Override
    public String getRowHeader(int row) {
        return "" + row;
    }

    @Override
    public int getDefinedRows() {
        return rowH.length;
    }

    @Override
    public int[] getColWidths() {
        return colW;
    }

    @Override
    public float getDefaultRowHeight() {
        return defRowH;
    }

    @Override
    public int getRowBufferSize() {
        return rowBufferSize;
    }

    @Override
    public int getColumnBufferSize() {
        return columnBufferSize;
    }

    @Override
    public int getMaxColumns() {
        return cols;
    }

    @Override
    public int getMaxRows() {
        return rows;
    }

    @Override
    public boolean isColumnHidden(int columnIndex) {
        return (hiddenColumnIndexes == null ? false : hiddenColumnIndexes.contains(columnIndex));
    }

    @Override
    public boolean isRowHidden(int rowIndex) {
        return (hiddenRowIndexes == null ? false : hiddenRowIndexes.contains(rowIndex));
    }

    @Override
    public boolean canResizeColumn() {
        return (!sheetProtected || !lockFormatColumns) && !touchMode;
    }

    @Override
    public boolean canResizeRow() {
        return (!sheetProtected || !lockFormatRows) && !touchMode;
    }

    public void setDisplayGridlines(boolean displayGridlines) {
        sheetWidget.setDisplayGridlines(displayGridlines);
    }

    public void setDisplayRowColHeadings(boolean displayRowColHeadings) {
        sheetWidget.setDisplayRowColHeadings(displayRowColHeadings);
    }

    public void refreshOverlayPositions() {
        sheetWidget.refreshAlwaysVisibleCellCommentOverlays();
        sheetWidget.refreshCurrentCellCommentOverlay();
        sheetWidget.refreshPopupButtonOverlays();
    }

    public void updateBottomRightCellValues(ArrayList<CellData> cellData) {
        sheetWidget.updateBottomRightCellValues(cellData);
    }

    public void updateTopLeftCellValues(ArrayList<CellData> cellData) {
        sheetWidget.updateTopLeftCellValues(cellData);
    }

    public void updateTopRightCellValues(ArrayList<CellData> cellData) {
        sheetWidget.updateTopRightCellValues(cellData);
    }

    public void updateBottomLeftCellValues(ArrayList<CellData> cellData) {
        sheetWidget.updateBottomLeftCellValues(cellData);
    }

    /**
     * This can contain values for any of the panes or values that are just in
     * the client side cache, but the cell is not actually visible.
     *
     * @param updatedCellData
     */
    public void cellValuesUpdated(ArrayList<CellData> updatedCellData) {
        sheetWidget.cellValuesUpdated(updatedCellData);
    }

    @Override
    public void setCellStyleWidthRatios(HashMap<Integer, Float> cellStyleWidthRatioMap) {
        spreadsheetHandler.setCellStyleWidthRatios(cellStyleWidthRatioMap);
    }

    @Override
    public void onSheetPaste(String text) {
        spreadsheetHandler.onPaste(text);
    }

    @Override
    public void clearSelectedCellsOnCut() {
        spreadsheetHandler.clearSelectedCellsOnCut();
    }

    @Override
    public Map<Integer, String> getConditionalFormattingStyles() {
        return conditionalFormattingStyles;
    }

    public void setConditionalFormattingStyles(HashMap<Integer, String> map) {
        conditionalFormattingStyles.clear();
        if (map != null) {
            conditionalFormattingStyles.putAll(map);
        }
    }

    public void selectCell(int col, int row, String value, boolean formula, boolean locked,
            boolean initialSelection) {
        selectionHandler.selectCell(col, row, value, formula, locked, initialSelection);
    }

    public void selectCellRange(int selectedCellColumn, int selectedCellRow, int firstColumn, int lastColumn,
            int firstRow, int lastRow, String value, boolean formula, boolean locked, boolean scroll) {
        selectionHandler.selectCellRange(selectedCellColumn, selectedCellRow, firstColumn, lastColumn, firstRow,
                lastRow, value, formula, locked, scroll);
    }

    public void refreshCellStyles() {
        getSheetWidget().refreshCellStyles();
    }

    @Override
    public boolean isTouchMode() {
        return touchMode;
    }

    public void setTouchMode(boolean touchMode) {
        this.touchMode = touchMode;
    }

    @Override
    public FormulaBarWidget getFormulaBarWidget() {
        return formulaBarWidget;
    }

    public void editCellComment(int col, int row) {
        sheetWidget.editCellComment(col, row);
    }

    @Override
    public void updateCellComment(String text, int col, int row) {
        spreadsheetHandler.updateCellComment(text, col, row);
    }

    @Override
    public void selectAll() {
        sheetWidget.setSelectedCell(1, 1);
        onSelectingCellsWithDrag(cols, rows);
        onFinishedSelectingCellsWithDrag(1, cols, 1, rows);
        updateSelectedCellValues(1, 1);
    }

    @Override
    public void focus() {
        focusSheet();
    }

    public void setCommsTrigger(CommsTrigger commsTrigger) {
        this.commsTrigger = commsTrigger;
    }

    private Timer delayedSending = new Timer() {

        @Override
        public void run() {
            commsTrigger.sendUpdates();
        }
    };

    void startDelayedSendingTimer() {
        delayedSending.schedule(DELAYED_SERVER_REQUEST_DELAY);
    }

    static int getTouchOrMouseClientX(Event event) {
        int scrollLeft = Document.get().getScrollLeft();
        if (WidgetUtil.isTouchEvent(event)) {
            return event.getChangedTouches().get(0).getClientX() + scrollLeft;
        } else {
            return event.getClientX() + scrollLeft;
        }
    }

    static int getTouchOrMouseClientY(Event event) {
        int scrollTop = Document.get().getScrollTop();
        if (WidgetUtil.isTouchEvent(event)) {
            return event.getChangedTouches().get(0).getClientY() + scrollTop;
        } else {
            return event.getClientY() + scrollTop;
        }
    }

    static int getTouchOrMouseClientY(NativeEvent currentGwtEvent) {
        return getTouchOrMouseClientY(Event.as(currentGwtEvent));
    }

    static int getTouchOrMouseClientX(NativeEvent event) {
        return getTouchOrMouseClientX(Event.as(event));
    }

    @Override
    public void setSheetFocused(boolean focused) {
        sheetWidget.setFocused(focused);
    }

    public void setId(String connectorId) {
        sheetWidget.postInit(connectorId);
    }

    @Override
    public String[] getSheetNames() {
        return sheetNames;
    }

    @Override
    public String getActiveSheetName() {
        return sheetNames[activeSheetIndex - 1];
    }

    @Override
    public void setGroupingCollapsed(boolean isCols, int colIndex, boolean collapsed) {
        spreadsheetHandler.setGroupingCollapsed(isCols, colIndex, collapsed);
    }

    @Override
    public void levelHeaderClicked(boolean cols, int level) {
        spreadsheetHandler.levelHeaderClicked(cols, level);
    }

    public void setColGroupingMax(int max) {
        sheetWidget.setColGroupingMax(max);
    }

    public void setRowGroupingMax(int max) {
        sheetWidget.setRowGroupingMax(max);
    }

    public void setColGroupingInversed(boolean inversed) {
        sheetWidget.setColGroupingInversed(inversed);
    }

    public void setRowGroupingInversed(boolean inversed) {
        sheetWidget.setRowGroupingInversed(inversed);
    }
}