Java tutorial
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.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; 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.Level; import java.util.logging.Logger; import com.google.gwt.canvas.client.Canvas; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptException; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.SpanElement; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.dom.client.StyleElement; import com.google.gwt.dom.client.Touch; import com.google.gwt.event.dom.client.ContextMenuEvent; import com.google.gwt.event.dom.client.ContextMenuHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Event.NativePreviewEvent; import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.EventListener; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; import com.vaadin.addon.spreadsheet.client.Cell.CellValueStyleKey; import com.vaadin.addon.spreadsheet.client.CopyPasteTextBox.CopyPasteHandler; import com.vaadin.addon.spreadsheet.shared.GroupingData; import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.BrowserInfo; import com.vaadin.client.ComputedStyle; import com.vaadin.client.MeasuredSize; import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.VLabel; import com.vaadin.client.ui.VLazyExecutor; import com.vaadin.client.ui.VOverlay; public class SheetWidget extends Panel { private static final String SELECTED_COLUMN_HEADER_CLASSNAME = "selected-column-header"; private static final String SELECTED_ROW_HEADER_CLASSNAME = "selected-row-header"; private static final String FREEZE_PANE_INACTIVE_STYLENAME = "inactive"; private static final String RESIZE_LINE_CLASSNAME = "resize-line"; private static final String ROW_RESIZING_CLASSNAME = "row-resizing"; private static final String COLUMN_RESIZING_CLASSNAME = "col-resizing"; private static final String RESIZE_TOOLTIP_LABEL_CLASSNAME = "v-spreadsheet-resize-tooltip-label"; private static final String HEADER_RESIZE_DND_FIRST_CLASSNAME = "header-resize-dnd-first"; private static final String HEADER_RESIZE_DND_SECOND_CLASSNAME = "header-resize-dnd-second"; private static final String HYPERLINK_TOOLTIP_LABEL_CLASSNAME = "v-spreadsheet-hyperlink-tooltip-label"; private static final String NO_GRIDLINES_CLASSNAME = "nogrid"; private static final String NO_ROWCOLHEADINGS_CLASSNAME = "noheaders"; private static final String CUSTOM_EDITOR_CELL_CLASSNAME = "custom-editor-cell"; private static final String CELL_RANGE_CLASSNAME = "cell-range"; private static final String CELL_SELECTION_CLASSNAME = "selected-cell-highlight"; static final String MERGED_CELL_CLASSNAME = "merged-cell"; private static final int CELL_COMMENT_OVERLAY_DELAY = 300; private static final int CELL_DATA_REQUESTER_DELAY = 100; private static final int SCROLL_HANDLER_TRIGGER_DELAY = 20; private static final String HEADER_RESIZE_DND_HTML = "<div class=\"" + HEADER_RESIZE_DND_FIRST_CLASSNAME + "\" ></div><div class=\"" + HEADER_RESIZE_DND_SECOND_CLASSNAME + "\" ></div>"; private static final String EDITING_CELL_STYLE = "{ display: inline !important;" + " outline: none !important; width: auto !important; z-index: -10; }"; private static final String HYPERLINK_CELL_STYLE = "{ cursor: pointer !important; }"; private static final String MERGED_REGION_CELL_STYLE = "{ display: none; }"; private static final String FREEZE_PANEL_OVERFLOW_STYLE = "{ overflow: hidden; }"; final Logger debugConsole = Logger.getLogger("spreadsheet-logger"); Map<CellValueStyleKey, Integer> scrollWidthCache = new HashMap<CellValueStyleKey, Integer>(); final SheetHandler actionHandler; private final SelectionWidget selectionWidget; private final VOverlay hyperlinkTooltip; private final VOverlay resizeTooltip; private final CellComment cellCommentOverlay; private CellComment focusedCellCommentOverlay; private final VLabel hyperlinkTooltipLabel; private final VLabel resizeTooltipLabel; /** Spreadsheet main (outmost) element */ DivElement spreadsheet = Document.get().createDivElement(); /** Sheet that will contain all the cells */ DivElement sheet = Document.get().createDivElement(); private HandlerRegistration previewHandlerRegistration; /** Header corner element that covers crossing headers */ private DivElement corner = Document.get().createDivElement(); private PasteAwareTextBox input; /** Invisible element for adjusting the scrollbars */ private final DivElement floater = Document.get().createDivElement(); /** A line to show the right/bottom dnd-resize position */ private final DivElement resizeLine = Document.get().createDivElement(); /** A line to show the left/top dnd-resize position */ private final DivElement resizeLineStable = Document.get().createDivElement(); /** * Div elements for row header divs. Note that index 0 in array points to * div on row 1 */ private ArrayList<DivElement> rowHeaders = new ArrayList<DivElement>(); private ArrayList<DivElement> frozenRowHeaders = new ArrayList<DivElement>(); /** * Div elements for column header divs. Note that index 0 in array points to * div on column 1 */ private ArrayList<DivElement> colHeaders = new ArrayList<DivElement>(); private ArrayList<DivElement> frozenColumnHeaders = new ArrayList<DivElement>(); /** * List of rows. Each row is a list of divs on that row. Note that index 0 * in the outer list points to row 1 and index 0 in the inner list points to * div in column 1. When frozen columns are used, this has the bottom right * pane's cells. */ private ArrayList<ArrayList<Cell>> rows = new ArrayList<ArrayList<Cell>>(); /** Cells in the frozen top left pane, starting from top left cell */ private ArrayList<Cell> topLeftCells = new ArrayList<Cell>(); /** Rows in the frozen top right pane */ private ArrayList<ArrayList<Cell>> topRightRows = new ArrayList<ArrayList<Cell>>(); /** Rows in the frozen bottom left pane */ private ArrayList<ArrayList<Cell>> bottomLeftRows = new ArrayList<ArrayList<Cell>>(); /** * Stylesheet element created for holding the dynamic row and column styles * (size position) */ private StyleElement cellSizeAndPositionStyle = Document.get().createStyleElement(); /** Stylesheet element for holding the workbook defined styles */ private StyleElement sheetStyle = Document.get().createStyleElement(); /** Stylesheet element for holding custom cell sizes (because of borders) */ private StyleElement shiftedBorderCellStyle = Document.get().createStyleElement(); /** * Stylesheet element for holding the edited cell style (for convenience * reasons, not actually visible). The selector is updated to the edited * cell. Also holds the style for the last freeze panel column, if any. This * is for preventing text oveflow over freeze panel. */ private StyleElement editedCellFreezeColumnStyle = Document.get().createStyleElement(); /** Stylesheet for cursor: pointer for hyperlink cells. Created on-demand. */ private StyleElement hyperlinkStyle; /** * Stylesheet for overriding column / row header sizes & positions when * dnd-resizing. */ private StyleElement resizeStyle = Document.get().createStyleElement(); /** * Stylesheet for hiding cells inside merged regions. */ private StyleElement mergedRegionStyle = Document.get().createStyleElement(); /** * An element used for counting the ppi. */ private DivElement ppiCounter = Document.get().createDivElement(); private DivElement topLeftPane = Document.get().createDivElement(); private DivElement topRightPane = Document.get().createDivElement(); private DivElement bottomLeftPane = Document.get().createDivElement(); private DivElement colGroupPane = Document.get().createDivElement(); private DivElement rowGroupPane = Document.get().createDivElement(); private DivElement colGroupFreezePane = Document.get().createDivElement(); private DivElement rowGroupFreezePane = Document.get().createDivElement(); private DivElement groupingCorner = Document.get().createDivElement(); private DivElement colGroupSummaryPane = Document.get().createDivElement(); private DivElement rowGroupSummaryPane = Document.get().createDivElement(); private DivElement colGroupBorderPane = Document.get().createDivElement(); private DivElement rowGroupBorderPane = Document.get().createDivElement(); /** * Hidden textfield that handles all copy and paste functions. */ private final CopyPasteTextBox copyPasteBox; /** * A dummy element for calculating the width each cell style need so need of * scientific notation can be calculated */ private SpanElement fontWidthDummyElement = Document.get().createSpanElement(); private final VLazyExecutor scrollHandler; private VLazyExecutor requester; SheetJsniUtil jsniUtil = GWT.create(SheetJsniUtil.class); private boolean touchMode; /** * Random id used as additional style for the widget element to connect * dynamic CSS rules to correct spreadsheet. */ private String sheetId; private final HashMap<String, CellData> cachedCellData; private Widget customEditorWidget; private HashMap<String, Widget> customWidgetMap; private HashMap<String, String> cellLinksMap; private Set<String> invalidFormulaCells; private HashMap<String, String> cellCommentsMap; private HashMap<String, String> cellCommentAuthorsMap; private HashMap<String, CellComment> alwaysVisibleCellComments; private HashMap<String, SheetOverlay> sheetOverlays; private HashMap<String, PopupButtonWidget> sheetPopupButtons; /** region ID to cell map */ private HashMap<Integer, MergedCell> mergedCells; private String cellCommentCellClassName; private int selectedCellCol; private int selectedCellRow; private boolean cellRangeStylesCleared = true; private boolean coherentSelection = true; private boolean customCellEditorDisplayed; private boolean editingCell; private boolean editingMergedCell; private boolean loaded; private boolean selectingCells; private int firstColumnIndex; private int firstColumnPosition; private int firstRowIndex; private int firstRowPosition; /** index of the last rendered column */ private int lastColumnIndex; /** right edge position of the last rendered column */ private int lastColumnPosition; /** index of the last rendered row */ private int lastRowIndex; /** *bottom edge position of the last rendered row */ private int lastRowPosition; private int previousScrollLeft; private int previousScrollTop; private int scrollViewHeight; private int scrollViewWidth; private int ppi; private int defRowH = -1; private int[] definedRowHeights; private int topFrozenPanelHeight; private int leftFrozenPanelWidth; /** 1-based. marks the last frozen row index */ int verticalSplitPosition; /** 1-based. marks the last frozen column index */ private int horizontalSplitPosition; private int resizedRowIndex = -1; private int resizedColumnIndex = -1; private int resizeFirstEdgePos; private int resizeLastEdgePos; private boolean resizingColumn; private boolean resizingRow; private boolean resized; private boolean columnResizeCancelled; private int cellCommentCellColumn = -1; private int cellCommentCellRow = -1; private int tempCol; private int tempRow; private boolean displayRowColHeadings; private Event mouseOverOrOutEvent; private HashMap<MergedRegion, Cell> overflownMergedCells; private List<GroupingData> groupingDataCol; private List<GroupingData> groupingDataRow; private int colGroupMax; private int rowGroupMax; private boolean colGroupInversed; private boolean rowGroupInversed; /* Bookkeeping for styling */ private Set<Cell> cellRangeStyledCells = new HashSet<Cell>(); private Set<CellCoord> cellRangeStyledCoords = new HashSet<CellCoord>(); private Set<Integer> selectedRowHeaderIndexes = new HashSet<Integer>(); private Set<Integer> selectedColHeaderIndexes = new HashSet<Integer>(); private Set<Integer> selectedFrozenRowHeaderIndexes = new HashSet<Integer>(); private Set<Integer> selectedFrozenColHeaderIndexes = new HashSet<Integer>(); private CellCoord highlightedCellCoord = null; private int calculatedRowGroupWidth; private int calculatedColGroupHeight; private String invalidFormulaMessage = null; static class CellCoord { private int col; private int row; public CellCoord(int col, int row) { this.col = col; this.row = row; } public int getCol() { return col; } public int getRow() { return row; } @Override public boolean equals(Object o) { if (o == null || !(o instanceof CellCoord)) { return false; } return row == ((CellCoord) o).getRow() && col == ((CellCoord) o).getCol(); } @Override public int hashCode() { int factor = (row + ((col + 1) / 2)); return 31 * (col + (factor * factor)); } } private VLazyExecutor cellCommentHandler = new VLazyExecutor(CELL_COMMENT_OVERLAY_DELAY, new ScheduledCommand() { @Override public void execute() { if (cellCommentCellColumn != -1 && cellCommentCellRow != -1) { showCellComment(cellCommentCellColumn, cellCommentCellRow); } } }); private VLazyExecutor onMouseOverOrOutHandler = new VLazyExecutor(100, new ScheduledCommand() { @Override public void execute() { if (isEditingCell()) { return; } Element target = mouseOverOrOutEvent.getEventTarget().cast(); boolean targetParentIsPaneElement = target.getParentElement().getAttribute("class").contains("sheet"); String className = target.getAttribute("class"); // cell comment lines are shown inside the sheet - skip // those if (className.startsWith(CellComment.COMMENT_OVERLAY_LINE_CLASSNAME)) { return; } if (className.contains("cell")) { className = className.substring(0, className.indexOf(" cell")); } if (className.equals(SheetOverlay.SHEET_IMAGE_CLASSNAME)) { target = mouseOverOrOutEvent.getCurrentEventTarget().cast(); className = target.getAttribute("class"); } else if (mouseOverOrOutEvent.getTypeInt() == Event.ONMOUSEOVER && targetParentIsPaneElement) { // because of cell overflow, the mouseover target might // be a // wrong cell jsniUtil.parseColRow(className); try { int parsedCol = jsniUtil.getParsedCol(); int parsedRow = jsniUtil.getParsedRow(); if (parsedCol == 0 || parsedRow == 0) { return; } target = getRealEventTargetCell(SpreadsheetWidget.getTouchOrMouseClientX(mouseOverOrOutEvent), SpreadsheetWidget.getTouchOrMouseClientY(mouseOverOrOutEvent), getCell(parsedCol, parsedRow)).getElement(); className = target.getAttribute("class"); if (className.contains("cell")) { className = className.substring(0, className.indexOf(" cell")); } } catch (JavaScriptException jse) { debugConsole.severe( "SheetWidget:onSheetMouseOverOrOut: JSE while trying to find real event target, className:" + className); } catch (IndexOutOfBoundsException ioobe) { debugConsole.warning( "SheetWidget:onSheetMouseOverOrOut: IOOBE while trying to find correct event target, className:" + className); } } jsniUtil.parseColRow(className); // if mouse moved to/from a comment mark triangle, or the // latest cell comment's cell, show/hide cell comment if (overlayShouldBeShownFor(className) || className.equals(cellCommentCellClassName) || cellHasComment(className) || cellHasInvalidFormula(className)) { updateCellCommentDisplay(mouseOverOrOutEvent, target); } else { if (!cellCommentEditMode && cellCommentOverlay.isShowing() && !className.contains("comment")) { Event.releaseCapture(sheet); cellCommentOverlay.hide(); cellCommentCellClassName = null; cellCommentCellColumn = -1; cellCommentCellRow = -1; } } if (targetParentIsPaneElement && cellLinksMap != null && cellLinksMap.containsKey(className)) { updateCellLinkTooltip(mouseOverOrOutEvent.getTypeInt(), jsniUtil.getParsedCol(), jsniUtil.getParsedRow(), cellLinksMap.get(className)); return; } else if (hyperlinkTooltip.isVisible()) { hyperlinkTooltip.hide(); } } }); private boolean overlayShouldBeShownFor(String className) { return className.equals(Cell.CELL_COMMENT_TRIANGLE_CLASSNAME) || className.equals(Cell.CELL_INVALID_FORMULA_CLASSNAME); } /** Height of the formula bar and column headers */ private int topOffset; /** Width of the row headers */ private int leftOffset; private boolean cellCommentEditMode; private CellComment currentlyEditedCellComment; private boolean crossedDown; private boolean crossedLeft; private boolean isMac; public SheetWidget(SheetHandler view, boolean touchMode) { String ua = BrowserInfo.getBrowserString().toLowerCase(); isMac = ua.contains("macintosh") || ua.contains("mac osx") || ua.contains("mac os x"); actionHandler = view; setTouchMode(touchMode); cachedCellData = new HashMap<String, CellData>(); alwaysVisibleCellComments = new HashMap<String, CellComment>(); sheetOverlays = new HashMap<String, SheetOverlay>(); mergedCells = new HashMap<Integer, MergedCell>(); overflownMergedCells = new HashMap<MergedRegion, Cell>(); hyperlinkTooltipLabel = new VLabel(); hyperlinkTooltipLabel.setStyleName(HYPERLINK_TOOLTIP_LABEL_CLASSNAME); hyperlinkTooltip = new VOverlay(); hyperlinkTooltip.setStyleName("v-tooltip"); hyperlinkTooltip.setOwner(this); hyperlinkTooltip.add(hyperlinkTooltipLabel); resizeTooltipLabel = new VLabel(); resizeTooltipLabel.setStyleName(RESIZE_TOOLTIP_LABEL_CLASSNAME); resizeTooltip = new VOverlay(); resizeTooltip.setStyleName("v-tooltip"); resizeTooltip.setOwner(this); resizeTooltip.add(resizeTooltipLabel); cellCommentOverlay = new CellComment(this, sheet); cellCommentOverlay.bringForward(); initDOM(); addStyleName("notfocused"); selectionWidget = new SelectionWidget(view, this); copyPasteBox = new CopyPasteTextBox(this, getCopyPasteHandler()); getElement().appendChild(copyPasteBox.getElement()); initListeners(); scrollHandler = new VLazyExecutor(SCROLL_HANDLER_TRIGGER_DELAY, new ScheduledCommand() { @Override public void execute() { if (loaded) { onSheetScroll(); } } }); requester = new VLazyExecutor(CELL_DATA_REQUESTER_DELAY, new ScheduledCommand() { @Override public void execute() { requestCells(); } }); } protected CopyPasteHandler getCopyPasteHandler() { return new CopyPasteHandlerImpl(this); } @Override protected void onAttach() { super.onAttach(); // we need to use the selectAll() method that needs GWT attachment if (copyPasteBox.getParent() == null) { adopt(copyPasteBox); } } public SheetHandler getSheetHandler() { return actionHandler; } protected SheetJsniUtil getSheetJsniUtil() { return jsniUtil; } @Override public void onUnload() { super.onUnload(); hyperlinkTooltip.hide(); resizeTooltip.hide(); } protected void requestCells() { actionHandler.onScrollViewChanged(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex); } /** * Set the model that stores the contents of the spreadsheet. Setting model * redraws the sheet. */ public void resetFromModel(final int scrollLeft, final int scrollTop) { loaded = false; cachedCellData.clear(); scrollWidthCache.clear(); if (ppiCounter.hasParentElement()) { ppi = ppiCounter.getOffsetWidth(); } removeCustomCellEditor(); selectionWidget.setPosition(1, 1, 1, 1); defRowH = -1; Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { if (ppi == 0 && ppiCounter.hasParentElement()) { ppi = ppiCounter.getOffsetWidth(); } updateSheetStyles(); updateCellStyles(); updateConditionalFormattingStyles(); resetScrollView(scrollLeft, scrollTop); resetRowAndColumnStyles(); actionHandler.onScrollViewChanged(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex); resetColHeaders(); resetRowHeaders(); updateColGrouping(); updateRowGrouping(); resetCellContents(); loaded = true; } }); } public void relayoutSheet(boolean triggerRequest) { updateSheetStyles(); int scrollTop = sheet.getScrollTop(); int scrollLeft = sheet.getScrollLeft(); int vScrollDiff = scrollTop - previousScrollTop; int hScrollDiff = scrollLeft - previousScrollLeft; try { // in case the number of cols/rows displayed has decreased if (lastRowIndex > actionHandler.getMaxRows()) { lastRowIndex = actionHandler.getMaxRows(); while ((lastRowIndex - firstRowIndex + 1) < rows.size()) { ArrayList<Cell> row = rows.remove(rows.size() - 1); for (Cell cell : row) { cell.getElement().removeFromParent(); } rowHeaders.remove(rowHeaders.size() - 1).removeFromParent(); } } if (lastColumnIndex > actionHandler.getMaxColumns()) { lastColumnIndex = actionHandler.getMaxColumns(); for (ArrayList<Cell> row : rows) { while ((lastColumnIndex - firstColumnIndex + 1) < row.size()) { row.remove(row.size() - 1).getElement().removeFromParent(); } } while ((lastColumnIndex - firstColumnIndex + 1) < colHeaders.size()) { colHeaders.remove(colHeaders.size() - 1).removeFromParent(); } } // the sizes of the currently displayed columns / rows may have // changed -> update styles and display more columns and/or rows if // necessary (scroll positions may have not changed) int newFirstRowPosition = 1; for (int i = 1; i < firstRowIndex; i++) { newFirstRowPosition += getRowHeight(i); if (i == verticalSplitPosition) { topFrozenPanelHeight = newFirstRowPosition; } } int newLastRowPosition = newFirstRowPosition; for (int i = firstRowIndex; i <= lastRowIndex; i++) { newLastRowPosition += getRowHeight(i); } final int bottomBound = topFrozenPanelHeight + scrollTop + scrollViewHeight + actionHandler.getRowBufferSize(); int topEdgeChange = newFirstRowPosition - firstRowPosition; int bottomEdgeChange = newLastRowPosition - lastRowPosition; firstRowPosition = newFirstRowPosition; lastRowPosition = newLastRowPosition; int newFirstColumnPosition = 0; for (int i = 1; i < firstColumnIndex; i++) { newFirstColumnPosition += actionHandler.getColWidthActual(i); if (horizontalSplitPosition == i) { leftFrozenPanelWidth = newFirstColumnPosition; } } int newLastColumnPosition = newFirstColumnPosition; for (int i = firstColumnIndex; i <= lastColumnIndex; i++) { newLastColumnPosition += actionHandler.getColWidthActual(i); } final int rightBound = leftFrozenPanelWidth + scrollLeft + scrollViewWidth + actionHandler.getColumnBufferSize(); int leftEdgeChange = newFirstColumnPosition - firstColumnPosition; int rightEdgeChange = newLastColumnPosition - lastColumnPosition; firstColumnPosition = newFirstColumnPosition; lastColumnPosition = newLastColumnPosition; // always call handle scroll left, otherwise // expanding groups with layouts does not work handleHorizontalScrollLeft(scrollLeft); updateCells(0, -1); if (rightEdgeChange < 0 || hScrollDiff > 0 || (lastColumnIndex < actionHandler.getMaxColumns() && lastColumnPosition < rightBound)) { handleHorizontalScrollRight(scrollLeft); updateCells(0, 1); } if (topEdgeChange > 0 || vScrollDiff < 0) { handleVerticalScrollUp(scrollTop); updateCells(-1, 0); } if (bottomEdgeChange != 0 || vScrollDiff > 0 || (lastRowIndex < actionHandler.getMaxRows() && lastRowPosition < bottomBound)) { handleVerticalScrollDown(scrollTop); updateCells(1, 0); } resetRowAndColumnStyles(); previousScrollLeft = scrollLeft; previousScrollTop = scrollTop; if (triggerRequest) { requester.trigger(); } // update the visible cell comment overlay positions for (CellComment cellComment : alwaysVisibleCellComments.values()) { if (actionHandler.isColumnHidden(cellComment.getCol()) || actionHandler.isRowHidden(cellComment.getRow())) { cellComment.hide(); } else { cellComment.refreshPositionAccordingToCellRightCorner(); } } moveHeadersToMatchScroll(); updateSelectionOutline(selectionWidget.getCol1(), selectionWidget.getCol2(), selectionWidget.getRow1(), selectionWidget.getRow2()); updateColGrouping(); updateRowGrouping(); updateOverflows(true); } catch (Exception e) { debugConsole.severe("SheetWidget:relayoutSheet: " + e.toString() + " while relayouting spreadsheet"); resetScrollView(scrollLeft, scrollTop); resetRowAndColumnStyles(); actionHandler.onScrollViewChanged(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex); resetColHeaders(); resetRowHeaders(); updateColGrouping(); updateRowGrouping(); resetCellContents(); refreshAlwaysVisibleCellCommentOverlays(); updateOverflownMergedCellSizes(); } } public void onWidgetResize() { if (loaded) { int newScrollViewHeight = sheet.getOffsetHeight(); int newScrollViewWidth = sheet.getOffsetWidth(); if (newScrollViewHeight > scrollViewHeight || newScrollViewWidth > scrollViewWidth) { scrollViewHeight = newScrollViewHeight; scrollViewWidth = newScrollViewWidth; // FIXME optimize. haxor to force sheet load more cells // vertically and horiz. previousScrollLeft = actionHandler.getColumnBufferSize() * -1; previousScrollTop = actionHandler.getRowBufferSize() * -1; scrollHandler.trigger(); } else { scrollViewHeight = newScrollViewHeight; scrollViewWidth = newScrollViewWidth; // no need to trigger scroll handler if the same size or smaller } // vaadin does bunch of layout phases so this needs to be done in // case the comment overlay position should be updated refreshAlwaysVisibleCellCommentOverlays(); } } /** Build DOM elements for this spreadsheet */ private void initDOM() { // Spreadsheet main element that acts as a viewport containing all the // other parts setElement(spreadsheet); spreadsheet.appendChild(sheet); spreadsheet.addClassName("v-spreadsheet"); // bottom-right-pane, always used sheet.setClassName("bottom-right-pane"); sheet.addClassName("sheet"); sheet.setTabIndex(3); // top right pane for cells. only used when needed. topRightPane.setClassName("top-right-pane"); topRightPane.addClassName("sheet"); spreadsheet.appendChild(topRightPane); // bottom left pane for cells. only used when needed. bottomLeftPane.setClassName("bottom-left-pane"); bottomLeftPane.addClassName("sheet"); spreadsheet.appendChild(bottomLeftPane); // top left pane for cells. only used when needed. topLeftPane.setClassName("top-left-pane"); topLeftPane.addClassName("sheet"); spreadsheet.appendChild(topLeftPane); // grouping cells colGroupPane.setClassName("col-group-pane"); spreadsheet.appendChild(colGroupPane); rowGroupPane.setClassName("row-group-pane"); spreadsheet.appendChild(rowGroupPane); colGroupFreezePane.setClassName("col-group-freeze-pane"); spreadsheet.appendChild(colGroupFreezePane); rowGroupFreezePane.setClassName("row-group-freeze-pane"); spreadsheet.appendChild(rowGroupFreezePane); rowGroupSummaryPane.setClassName("row-group-summary"); spreadsheet.appendChild(rowGroupSummaryPane); colGroupSummaryPane.setClassName("col-group-summary"); spreadsheet.appendChild(colGroupSummaryPane); colGroupBorderPane.setClassName("col-group-border"); spreadsheet.appendChild(colGroupBorderPane); rowGroupBorderPane.setClassName("row-group-border"); spreadsheet.appendChild(rowGroupBorderPane); groupingCorner.setClassName("grouping-corner"); spreadsheet.appendChild(groupingCorner); resizeLine.setClassName(RESIZE_LINE_CLASSNAME); spreadsheet.appendChild(resizeLine); resizeLineStable.setClassName(RESIZE_LINE_CLASSNAME); sheet.appendChild(resizeLineStable); // Corner div corner.setClassName("corner"); spreadsheet.appendChild(corner); // floater, extra element for adjusting scroll bars correctly floater.setClassName("floater"); // input input = new PasteAwareTextBox(this); input.setWidth("0"); input.setValue("x"); input.getElement().setId("cellinput"); DOM.appendChild(sheet, input.getElement()); adopt(input); // extra element for counting the pixels per inch so points can be // converted to pixels ppiCounter.getStyle().setWidth(1, Unit.IN); ppiCounter.getStyle().setPosition(Position.ABSOLUTE); ppiCounter.getStyle().setVisibility(Visibility.HIDDEN); ppiCounter.getStyle().setPadding(0, Unit.PX); spreadsheet.appendChild(ppiCounter); // extra element for counting the width in pixels each cell style needs // for showing numbers and applying scientific notation. fontWidthDummyElement.getStyle().setVisibility(Visibility.HIDDEN); fontWidthDummyElement.setInnerText("5555555555"); } void postInit(String connectorId) { sheetId = "spreadsheet-" + connectorId; spreadsheet.addClassName(sheetId); // Dynamic position & size styles for this spreadsheet cellSizeAndPositionStyle.setType("text/css"); cellSizeAndPositionStyle.setId(sheetId + "-dynamicStyle"); Document.get().getBody().getParentElement().getFirstChild().appendChild(cellSizeAndPositionStyle); // Workbook styles sheetStyle.setType("text/css"); sheetStyle.setId(sheetId + "-sheetStyle"); cellSizeAndPositionStyle.getParentElement().appendChild(sheetStyle); // Custom cell size styles (because of borders) shiftedBorderCellStyle.setType("text/css"); shiftedBorderCellStyle.setId(sheetId + "-customCellSizeStyle"); cellSizeAndPositionStyle.getParentElement().appendChild(shiftedBorderCellStyle); // style for "hiding" the edited cell editedCellFreezeColumnStyle.setType("text/css"); editedCellFreezeColumnStyle.setId(sheetId + "-editedCellStyle"); cellSizeAndPositionStyle.getParentElement().appendChild(editedCellFreezeColumnStyle); jsniUtil.insertRule(editedCellFreezeColumnStyle, ".notusedselector" + EDITING_CELL_STYLE); jsniUtil.insertRule(editedCellFreezeColumnStyle, ".notusedselector" + FREEZE_PANEL_OVERFLOW_STYLE); // style for hiding the cell inside merged regions mergedRegionStyle.setType("text/css"); mergedRegionStyle.setId(sheetId + "-mergedRegionStyle"); cellSizeAndPositionStyle.getParentElement().appendChild(mergedRegionStyle); resizeStyle.setType("text/css"); resizeStyle.setId(sheetId + "-resizeStyle"); cellSizeAndPositionStyle.getParentElement().appendChild(resizeStyle); } /** * Remove sheet DOM elements created. Currently does not clear Frozen panes' * contents - those are being handled when reloading sheet. FIXME unify * clearing of all DOM elements when reloading. */ private void cleanDOM() { floater.removeFromParent(); for (DivElement header : colHeaders) { header.removeFromParent(); } colHeaders.clear(); for (DivElement header : rowHeaders) { header.removeFromParent(); } rowHeaders.clear(); for (ArrayList<Cell> row : rows) { for (Cell cell : row) { cell.getElement().removeFromParent(); } row.clear(); } rows.clear(); } /** For internal use. May be removed at a later time. */ public void removeStyles() { // Remove style tags cellSizeAndPositionStyle.removeFromParent(); sheetStyle.removeFromParent(); shiftedBorderCellStyle.removeFromParent(); editedCellFreezeColumnStyle.removeFromParent(); resizeStyle.removeFromParent(); mergedRegionStyle.removeFromParent(); if (hyperlinkStyle != null) { hyperlinkStyle.removeFromParent(); } } protected void onSheetScroll(Event event) { scrollHandler.trigger(); moveHeadersToMatchScroll(); updateOverflownMergedCellSizes(); refreshAlwaysVisibleCellCommentOverlays(); refreshPopupButtonOverlays(); } /** * This is using a delayed execution because we don't want to try to do * stuff when it is unnecessary. * * @param event * @param sheetPaneElement */ protected void onSheetMouseOverOrOut(Event event) { mouseOverOrOutEvent = event; onMouseOverOrOutHandler.trigger(); } protected void onSheetMouseMove(Event event) { if (!cellCommentEditMode && cellCommentCellColumn != -1 && cellCommentCellRow != -1) { // the comment should only be displayed after the // mouse has "stopped" on top of a cell with a comment cellCommentHandler.trigger(); } } protected boolean isEventInCustomEditorCell(Event event) { if (customEditorWidget != null) { final Element target = event.getEventTarget().cast(); final Element customWidgetElement = customEditorWidget.getElement(); return (customWidgetElement.isOrHasChild(target) || customWidgetElement.getParentElement() != null && customWidgetElement.getParentElement().isOrHasChild(target)); } return false; } protected Cell getRealEventTargetCell(final int clientX, final int clientY, final Cell cell) { Cell mergedCell = getMergedCell(toKey(cell.getCol(), cell.getRow())); if (mergedCell == null) { Element target = cell.getElement(); int newX = cell.getCol(); int newY = cell.getRow(); boolean changed = false; if (clientX < target.getAbsoluteLeft() && cell.getCol() > firstColumnIndex) { newX = cell.getCol() - 1; changed = true; } else if (clientX > target.getAbsoluteRight() && cell.getCol() < lastColumnIndex) { newX = cell.getCol() + 1; changed = true; } if (clientY < target.getAbsoluteTop() && cell.getRow() > firstRowIndex) { newY = cell.getRow() - 1; changed = true; } else if (clientY > target.getAbsoluteBottom() && cell.getRow() < lastRowIndex) { newY = cell.getRow() + 1; changed = true; } if (changed) { return getRealEventTargetCell(clientX, clientY, getCell(newX, newY)); } return cell; } else { return mergedCell; } } /** * * @param target * The clicked element * @param event * The original event (that can be onClick or onTouchStart) */ protected void onSheetMouseDown(Event event) { Element target = event.getEventTarget().cast(); String className = target.getAttribute("class"); // click target is the inner div because IE10 and 9 are not compatible // with 'pointer-events: none' if ((BrowserInfo.get().isIE9() || BrowserInfo.get().isIE10()) && (className == null || className.isEmpty())) { String parentClassName = target.getParentElement().getAttribute("class"); if (parentClassName.contains("cell")) { className = parentClassName; } } if (cellCommentEditMode && !className.contains("comment-overlay")) { cellCommentEditMode = false; currentlyEditedCellComment.setEditMode(false); if (currentlyEditedCellComment.equals(cellCommentOverlay)) { cellCommentOverlay.hide(); cellCommentCellClassName = null; cellCommentCellColumn = -1; cellCommentCellRow = -1; } } if (className.contains("sheet") || target.getTagName().equals("input") || className.equals("floater")) { return; // event target is one of the panes or input } if (isEventInCustomEditorCell(event)) { // allow sheet context menu on top of custom editors if (event.getButton() == NativeEvent.BUTTON_RIGHT) { actionHandler.onCellRightClick(event, selectedCellCol, selectedCellRow); } else if (selectingCells) { // this is probably unnecessary stoppedSelectingCellsWithDrag(event); } } else if (className.contains("cell")) { if (className.equals("cell-comment-triangle")) { jsniUtil.parseColRow(target.getParentElement().getAttribute("class")); } else { jsniUtil.parseColRow(className); } int targetCol = jsniUtil.getParsedCol(); int targetRow = jsniUtil.getParsedRow(); // because of text overflow, the click might have happened on // top of a another cell than what event has. // merged cells are a special case, text won't overflow -> skip try { if (!className.endsWith(MERGED_CELL_CLASSNAME)) { int clientX = SpreadsheetWidget.getTouchOrMouseClientX(event); int clientY = SpreadsheetWidget.getTouchOrMouseClientY(event); Cell targetCell = getRealEventTargetCell(clientX, clientY, getCell(targetCol, targetRow)); target = targetCell.getElement(); targetCol = targetCell.getCol(); targetRow = targetCell.getRow(); } } catch (JavaScriptException jse) { debugConsole.severe( "SheetWidget:onSheetMouseDown - JSE while trying to find real event target, className:" + className); } catch (IndexOutOfBoundsException ioobe) { debugConsole.severe( "SheetWidget:onSheetMouseDown - IOOBE while trying to find real event target, className:" + className); } event.stopPropagation(); event.preventDefault(); if (event.getButton() == NativeEvent.BUTTON_RIGHT) { Event.releaseCapture(sheet); actionHandler.onCellRightClick(event, targetCol, targetRow); } else { sheet.focus(); // quit input if active if (editingCell && !input.getElement().isOrHasChild(target)) { actionHandler.onCellInputBlur(input.getValue()); } if (event.getCtrlKey() || event.getMetaKey() || event.getShiftKey()) { actionHandler.onCellClick(targetCol, targetRow, target.getInnerText(), event.getShiftKey(), event.getMetaKey() || event.getCtrlKey(), true); tempCol = -1; tempRow = -1; } else { // no special keys used // link cells are special keys // TODO should investigate what is the correct action when // clicking on hyperlink cells that overflow to next cells if (cellLinksMap != null && cellLinksMap.containsKey(toKey(jsniUtil.getParsedCol(), jsniUtil.getParsedRow()))) { actionHandler.onLinkCellClick(targetCol, targetRow); } else { // otherwise selecting starts actionHandler.onCellClick(targetCol, targetRow, target.getInnerText(), event.getShiftKey(), event.getMetaKey() || event.getCtrlKey(), false); selectingCells = true; tempCol = targetCol; tempRow = targetRow; startCellTopLeft = isCellRenderedInTopLeftPane(targetCol, targetRow); startCellTopRight = isCellRenderedInTopRightPane(targetCol, targetRow); startCellBottomLeft = isCellRenderedInBottomLeftPane(targetCol, targetRow); crossedDown = !startCellTopLeft && !startCellTopRight; crossedLeft = !startCellTopLeft && !startCellBottomLeft; clientX = SpreadsheetWidget.getTouchOrMouseClientX(event); clientY = SpreadsheetWidget.getTouchOrMouseClientY(event); Event.setCapture(sheet); } } } } } protected void onMouseMoveWhenSelectingCells(Event event) { final Element target; /* * Touch events handle target element differently. According to specs, * Touch.getTarget() is the equivalent of event.getTarget(). Of course, * Safari doesn't follow the specifications; all target references are * to the element where we started the drag. * * We need to manually parse x/y coords in #getRealEventTargetCell() to * find the correct cell. */ if (event.getChangedTouches() != null && event.getChangedTouches().length() > 0) { JsArray<Touch> touches = event.getChangedTouches(); target = touches.get(touches.length() - 1).getTarget().cast(); } else if (event.getTouches() != null && event.getTouches().length() > 0) { JsArray<Touch> touches = event.getTouches(); target = touches.get(touches.length() - 1).getTarget().cast(); } else { target = event.getEventTarget().cast(); } // Update scroll deltas int y = SpreadsheetWidget.getTouchOrMouseClientY(event); int x = SpreadsheetWidget.getTouchOrMouseClientX(event); if (checkScrollWhileSelecting(y, x)) { return; } int col = 0, row = 0; String className = null; if (target != null) { className = target.getAttribute("class"); /* * Parse according to classname of target element. As said above, * Safari gives us the wrong target and hence we have the wrong * style name here. * * This also means that if we move outside the sheet, we continue * execution past this check. */ jsniUtil.parseColRow(className); col = jsniUtil.getParsedCol(); row = jsniUtil.getParsedRow(); } if (row == 0 || col == 0) { return; } // skip search of actual cell if this is a merged cell if (!className.endsWith(MERGED_CELL_CLASSNAME)) { Cell targetCell = getRealEventTargetCell(x, y, getCell(col, row)); col = targetCell.getCol(); row = targetCell.getRow(); } if (col != tempCol || row != tempRow) { if (col == 0) { // on top of scroll bar if (x > target.getParentElement().getAbsoluteRight()) { col = getRightVisibleColumnIndex() + 1; } else { col = tempCol; } } if (row == 0) { if (y > sheet.getAbsoluteBottom()) { row = getBottomVisibleRowIndex() + 1; } else { row = tempRow; } } actionHandler.onSelectingCellsWithDrag(col, row); tempCol = col; tempRow = row; } } private boolean checkScrollWhileSelecting(int y, int x) { int scrollPaneTop = sheet.getAbsoluteTop(); int scrollPaneLeft = sheet.getAbsoluteLeft(); int scrollPaneBottom = sheet.getAbsoluteBottom(); int scrollPaneRight = sheet.getAbsoluteRight(); clientX = x; clientY = y; if (y < scrollPaneTop) { if (crossedDown || (!startCellTopRight && !startCellTopLeft)) { deltaY = y - scrollPaneTop; } } else if (y > scrollPaneBottom) { deltaY = y - scrollPaneBottom; } else { deltaY = 0; } if (x < scrollPaneLeft) { if (crossedLeft || (!startCellBottomLeft && !startCellTopLeft)) { deltaX = x - scrollPaneLeft; } } else if (x > scrollPaneRight) { deltaX = x - scrollPaneRight; } else { deltaX = 0; } // If we're crossing the top freeze pane border to the scroll area, the // bottom part must be scrolled all the way up. boolean scrolled = false; if (sheet.getScrollTop() != 0) { boolean mouseOnTopSide = y < scrollPaneTop; if (!crossedDown && (startCellTopLeft || startCellTopRight) && isCellRenderedInFrozenPane(tempCol, tempRow) && !mouseOnTopSide) { sheet.setScrollTop(0); onSheetScroll(null); crossedDown = true; scrolled = true; } } // If we're crossing the left freeze pane border, the right-hand part // must be scrolled all the way to the left. if (sheet.getScrollLeft() != 0) { boolean mouseOnLeftSide = x < scrollPaneLeft; if (!crossedLeft && (startCellTopLeft || startCellBottomLeft) && isCellRenderedInFrozenPane(tempCol, tempRow) && !mouseOnLeftSide) { sheet.setScrollLeft(0); onSheetScroll(null); crossedLeft = true; scrolled = true; } } if ((deltaY < 0 && sheet.getScrollTop() != 0) || deltaY > 0 || (deltaX < 0 && sheet.getScrollLeft() != 0) || deltaX > 0) { startScrollTimer(); scrolled = true; } // If the sheet was scrolled due to crossing freeze pane borders during // drag selection, the actual selection event will be handled on the // next mouse move event. if (scrolled) { return true; } else { stopScrollTimer(); return false; } } protected void stoppedSelectingCellsWithDrag(Event event) { stopScrollTimer(); Event.releaseCapture(sheet); if ((selectedCellCol != tempCol || selectedCellRow != tempRow) && tempCol != -1 && tempRow != -1) { actionHandler.onFinishedSelectingCellsWithDrag(selectedCellCol, tempCol, selectedCellRow, tempRow); } else { actionHandler.onCellClick(tempCol, tempRow, ((Element) event.getEventTarget().cast()).getInnerText(), event.getShiftKey(), event.getMetaKey() || event.getCtrlKey(), true); } selectingCells = false; tempCol = -1; tempRow = -1; } final int TOP_LEFT_SELECTION_OFFSET = 5; final int BOTTOM_RIGHT_SELECTION_OFFSET = 25; private boolean startCellTopLeft, startCellTopRight, startCellBottomLeft; private int deltaX, deltaY, clientX, clientY; private boolean scrollTimerRunning; private Timer scrollTimer = new Timer() { @Override public void run() { // Handle scrolling sheet.setScrollTop(sheet.getScrollTop() + deltaY / 2); sheet.setScrollLeft(sheet.getScrollLeft() + deltaX / 2); onSheetScroll(null); // Determine selection point int selectionPointX = clientX; int selectionPointY = clientY; if (deltaX < 0) { selectionPointX = sheet.getAbsoluteLeft() + TOP_LEFT_SELECTION_OFFSET; } else if (deltaX > 0) { selectionPointX = sheet.getAbsoluteRight() - BOTTOM_RIGHT_SELECTION_OFFSET; } if (deltaY < 0) { selectionPointY = sheet.getAbsoluteTop() + TOP_LEFT_SELECTION_OFFSET; } else if (deltaY > 0) { selectionPointY = sheet.getAbsoluteBottom() - BOTTOM_RIGHT_SELECTION_OFFSET; } // Adjust selection point if we have reached scroll top if (deltaY != 0 && sheet.getScrollTop() == 0) { MeasuredSize ms = new MeasuredSize(); ms.measure(spreadsheet); int minimumTop = spreadsheet.getAbsoluteTop() + ms.getPaddingTop() + TOP_LEFT_SELECTION_OFFSET; if (clientY > minimumTop) { selectionPointY = clientY; } else { selectionPointY = minimumTop; } } // Adjust selection point if we have reached scroll left if (deltaX != 0 && sheet.getScrollLeft() == 0) { MeasuredSize ms = new MeasuredSize(); ms.measure(spreadsheet); int minimumLeft = spreadsheet.getAbsoluteLeft() + ms.getPaddingLeft() + TOP_LEFT_SELECTION_OFFSET; if (clientX > minimumLeft) { selectionPointX = clientX; } else { selectionPointX = minimumLeft; } } // Handle selection handleSelectionOnScroll(selectionPointX, selectionPointY); } }; private void handleSelectionOnScroll(int selectionPointX, int selectionPointY) { Element target = WidgetUtil.getElementFromPoint(selectionPointX, selectionPointY); if (target != null) { final String className = target.getAttribute("class"); jsniUtil.parseColRow(className); int col = jsniUtil.getParsedCol(); int row = jsniUtil.getParsedRow(); if (col != 0 && row != 0) { actionHandler.onSelectingCellsWithDrag(col, row); tempCol = col; tempRow = row; } } } private void startScrollTimer() { if (!scrollTimerRunning) { scrollTimerRunning = true; scrollTimer.scheduleRepeating(50); } } private void stopScrollTimer() { deltaX = 0; deltaY = 0; scrollTimer.cancel(); scrollTimerRunning = false; } private void initListeners() { SheetEventListener listener = GWT.create(SheetEventListener.class); listener.setSheetWidget(this); listener.setSheetPaneElement(topLeftPane, topRightPane, bottomLeftPane, sheet); // for some reason the click event is not fired normally for headers previewHandlerRegistration = Event.addNativePreviewHandler(new NativePreviewHandler() { @Override public void onPreviewNativeEvent(NativePreviewEvent event) { int eventTypeInt = event.getTypeInt(); final NativeEvent nativeEvent = event.getNativeEvent(); Element target = nativeEvent.getEventTarget().cast(); String className = target.getAttribute("class"); if (getElement().isOrHasChild((Node) nativeEvent.getEventTarget().cast())) { if (Event.ONTOUCHSTART == eventTypeInt || Event.ONMOUSEDOWN == eventTypeInt || Event.ONMOUSEUP == eventTypeInt || Event.ONDBLCLICK == eventTypeInt || Event.ONCLICK == eventTypeInt) { setFocused(true); } } if ((resizingColumn || resizingRow) && eventTypeInt == Event.ONMOUSEMOVE) { if (resizedColumnIndex != -1) { handleColumnResizeDrag(SpreadsheetWidget.getTouchOrMouseClientX(nativeEvent), SpreadsheetWidget.getTouchOrMouseClientY(nativeEvent)); } else if (resizedRowIndex != -1) { handleRowResizeDrag(SpreadsheetWidget.getTouchOrMouseClientX(nativeEvent), SpreadsheetWidget.getTouchOrMouseClientY(nativeEvent)); } else { resizingColumn = false; resizingRow = false; } event.cancel(); } else if (eventTypeInt == Event.ONMOUSEUP && canResize(target)) { if (resizingColumn || resizingRow || className.equals(HEADER_RESIZE_DND_FIRST_CLASSNAME) || className.equals(HEADER_RESIZE_DND_SECOND_CLASSNAME)) { columnResizeCancelled = true; resizingColumn = false; resizingRow = false; jsniUtil.clearCSSRules(resizeStyle); resizeTooltip.hide(); event.cancel(); if (resizedColumnIndex != -1) { spreadsheet.removeClassName(COLUMN_RESIZING_CLASSNAME); stopColumnResizeDrag(SpreadsheetWidget.getTouchOrMouseClientX(event.getNativeEvent())); } else if (resizedRowIndex != -1) { spreadsheet.removeClassName(ROW_RESIZING_CLASSNAME); stopRowResizeDrag(SpreadsheetWidget.getTouchOrMouseClientY(event.getNativeEvent())); } } } else { if (getElement().isOrHasChild(target)) { if (eventTypeInt == Event.ONCLICK) { int i = jsniUtil.isHeader(className); if (i == 1 || i == 2) { int index = jsniUtil.parseHeaderIndex(className); if (i == 1) { actionHandler.onRowHeaderClick(index, nativeEvent.getShiftKey(), nativeEvent.getMetaKey() || nativeEvent.getCtrlKey()); } else { actionHandler.onColumnHeaderClick(index, nativeEvent.getShiftKey(), nativeEvent.getMetaKey() || nativeEvent.getCtrlKey()); } event.cancel(); sheet.focus(); } } else if (eventTypeInt == Event.ONMOUSEDOWN && canResize(target)) { if (className.equals(HEADER_RESIZE_DND_FIRST_CLASSNAME)) { className = target.getParentElement().getAttribute("class"); int i = jsniUtil.isHeader(className); if (i == 1) { // row i = jsniUtil.parseHeaderIndex(className); startRowResizeDrag(i - 1, SpreadsheetWidget.getTouchOrMouseClientX(nativeEvent), SpreadsheetWidget.getTouchOrMouseClientY(nativeEvent)); } else if (i == 2) { // col i = jsniUtil.parseHeaderIndex(className); columnResizeCancelled = false; startColumnResizeDrag(i - 1, SpreadsheetWidget.getTouchOrMouseClientX(nativeEvent), SpreadsheetWidget.getTouchOrMouseClientY(nativeEvent)); } event.cancel(); } else if (className.equals(HEADER_RESIZE_DND_SECOND_CLASSNAME)) { className = target.getParentElement().getAttribute("class"); int i = jsniUtil.isHeader(className); if (i == 1) { // row i = jsniUtil.parseHeaderIndex(className); startRowResizeDrag(i, SpreadsheetWidget.getTouchOrMouseClientX(nativeEvent), SpreadsheetWidget.getTouchOrMouseClientY(nativeEvent)); } else if (i == 2) { // col i = jsniUtil.parseHeaderIndex(className); columnResizeCancelled = false; startColumnResizeDrag(i, SpreadsheetWidget.getTouchOrMouseClientX(nativeEvent), SpreadsheetWidget.getTouchOrMouseClientY(nativeEvent)); } event.cancel(); } } else if (eventTypeInt == Event.ONDBLCLICK && canResize(target)) { if (className.equals(HEADER_RESIZE_DND_FIRST_CLASSNAME)) { className = target.getParentElement().getAttribute("class"); int i = jsniUtil.isHeader(className); if (i == 1) { // row // autofit row ??? } else if (i == 2) { // col i = jsniUtil.parseHeaderIndex(className) - 1; while (actionHandler.isColumnHidden(i) && i > 0) { i--; } if (i > 0) { actionHandler.onColumnHeaderResizeDoubleClick(i); } } event.cancel(); } else if (className.equals(HEADER_RESIZE_DND_SECOND_CLASSNAME)) { className = target.getParentElement().getAttribute("class"); int i = jsniUtil.isHeader(className); if (i == 1) { // row // autofit row ??? } else if (i == 2) { // col i = jsniUtil.parseHeaderIndex(className); while (actionHandler.isColumnHidden(i) && i > 0) { i--; } if (i > 0) { actionHandler.onColumnHeaderResizeDoubleClick(i); } } event.cancel(); } } } } } private boolean canResize(Element target) { int i = isHeader(target); if (resizingRow || i == 1) { return actionHandler.canResizeRow(); } else if (resizingColumn || i == 2) { return actionHandler.canResizeColumn(); } return false; } /** * returns 1 for row 2 for column 0 for not header * * @see {@link SheetJsniUtil.isHeader(String)} */ private int isHeader(Element target) { String className = target.getParentElement().getAttribute("class"); return jsniUtil.isHeader(className); } }); addDomHandler(new ContextMenuHandler() { @Override public void onContextMenu(ContextMenuEvent event) { if (actionHandler.hasCustomContextMenu()) { Element target = event.getNativeEvent().getEventTarget().cast(); String className = target.getAttribute("class"); int i = jsniUtil.isHeader(className); if (i == 1 || i == 2) { int index = jsniUtil.parseHeaderIndex(className); if (i == 1) { actionHandler.onRowHeaderRightClick(event.getNativeEvent(), index); } else { actionHandler.onColumnHeaderRightClick(event.getNativeEvent(), index); } } event.preventDefault(); event.stopPropagation(); } } }, ContextMenuEvent.getType()); } protected boolean isEditingCell() { return editingCell; } protected boolean isMouseButtonDownAndSelecting() { return selectingCells; } private void startRowResizeDrag(int rowIndex, int clientX, int clientY) { // for some reason FF doesn't hide headers instantly, // the event might be from hidden div while (actionHandler.isRowHidden(rowIndex)) { rowIndex--; } if (rowIndex == 0) { // ERROR ... return; } Event.setCapture(getElement()); resizingRow = true; resized = false; resizedRowIndex = rowIndex; resizedColumnIndex = -1; DivElement header; if (resizedRowIndex <= verticalSplitPosition) { header = frozenRowHeaders.get(resizedRowIndex - 1); } else { header = rowHeaders.get(rowIndex - firstRowIndex); } resizeFirstEdgePos = header.getAbsoluteTop(); resizeLastEdgePos = header.getAbsoluteBottom(); if (actionHandler.getRowHeight(rowIndex) > 0) { resizeTooltipLabel.setText("Height: " + actionHandler.getRowHeight(rowIndex) + "pt"); } else { resizeTooltipLabel.setText("Hide row"); } showResizeTooltipRelativeTo(clientX, clientY); resizeTooltip.show(); spreadsheet.addClassName(ROW_RESIZING_CLASSNAME); resizeLineStable.addClassName("row" + rowIndex); rowIndex++; while (rowIndex < actionHandler.getMaxRows() && actionHandler.isRowHidden(rowIndex)) { rowIndex++; } resizeLine.addClassName("rh row" + (rowIndex)); handleRowResizeDrag(clientX, clientY); } private void startColumnResizeDrag(final int columnIndex, final int clientX, final int clientY) { resized = false; Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { if (columnResizeCancelled) { return; } // for some reason FF doesn't hide headers instantly, // the event might be from hidden div int tempColumnIndex = columnIndex; while (actionHandler.isColumnHidden(tempColumnIndex)) { tempColumnIndex--; } if (tempColumnIndex < 1) { // ERROR ... return; } Event.setCapture(getElement()); resizingColumn = true; resizedColumnIndex = tempColumnIndex; resizedRowIndex = -1; DivElement header; if (resizedColumnIndex <= horizontalSplitPosition) { header = frozenColumnHeaders.get(resizedColumnIndex - 1); } else { header = colHeaders.get(tempColumnIndex - firstColumnIndex); } resizeFirstEdgePos = header.getAbsoluteLeft(); resizeLastEdgePos = header.getAbsoluteRight(); if (actionHandler.getColWidth(tempColumnIndex) > 0) { resizeTooltipLabel.setText("Width: " + actionHandler.getColWidth(tempColumnIndex) + "px"); } else { resizeTooltipLabel.setText("Hide column"); } showResizeTooltipRelativeTo(clientX, clientY); resizeTooltip.show(); spreadsheet.addClassName(COLUMN_RESIZING_CLASSNAME); resizeLineStable.addClassName("col" + tempColumnIndex); tempColumnIndex++; while (columnIndex <= actionHandler.getMaxColumns() && actionHandler.isColumnHidden(tempColumnIndex)) { tempColumnIndex++; } resizeLine.addClassName("ch col" + (tempColumnIndex)); handleColumnResizeDrag(clientX, clientY); } }); } private void stopRowResizeDrag(int clientY) { Event.releaseCapture(getElement()); resizeLine.setClassName(RESIZE_LINE_CLASSNAME); selectionWidget.getElement().getStyle().clearMarginTop(); resizeLineStable.removeClassName("row" + resizedRowIndex); if (resized) { final Map<Integer, Float> newSizesForPOI = new HashMap<Integer, Float>(); int px = clientY - resizeFirstEdgePos; float pt = convertPixelsToPoint(px); // Do not allow negative sizeslibre if (pt < 0) { pt = 0; } if (pt != actionHandler.getRowHeight(resizedRowIndex)) { newSizesForPOI.put(resizedRowIndex, pt); } if (!newSizesForPOI.isEmpty()) { actionHandler.onRowsResized(newSizesForPOI); } } resizedRowIndex = -1; } private void stopColumnResizeDrag(int clientX) { Event.releaseCapture(getElement()); resizeLine.setClassName(RESIZE_LINE_CLASSNAME); resizeLineStable.removeClassName("col" + resizedColumnIndex); selectionWidget.getElement().getStyle().clearMarginLeft(); if (resized) { final Map<Integer, Integer> newSizes = new HashMap<Integer, Integer>(); int px = clientX - resizeFirstEdgePos; // Do not allow negative sizes if (px < 0) { px = 0; } if (px != actionHandler.getColWidthActual(resizedColumnIndex)) { newSizes.put(resizedColumnIndex, px); } if (!newSizes.isEmpty()) { actionHandler.onColumnsResized(newSizes); } } // TODO scroll into view resizedColumnIndex = -1; } private void handleRowResizeDrag(int clientX, int clientY) { resized = true; int delta = clientY - resizeFirstEdgePos; if (delta < 0) { delta = 0; } jsniUtil.clearCSSRules(resizeStyle); String rule; // only the dragged header size has changed. if (delta > 0) { resizeTooltipLabel.setText("Height: " + delta + "px " + convertPixelsToPoint(delta) + "pt"); } else { resizeTooltipLabel.setText("Hide row"); } // enter custom size for the resized row header rule = "." + sheetId + " > div.rh.row" + resizedRowIndex + "{height:" + delta + "px;}"; jsniUtil.insertRule(resizeStyle, rule); int headersAfter = 0; int spaceAfter = sheet.getAbsoluteBottom() - clientY; // might need to add more headers // count how many headers after resize one for (int i = resizedRowIndex + 1; i <= lastRowIndex && headersAfter < spaceAfter; i++) { headersAfter += getRowHeight(i); } // adjust headers after resized one with margin int margin = clientY - resizeLastEdgePos; if (margin < resizeFirstEdgePos - resizeLastEdgePos) { margin = resizeFirstEdgePos - resizeLastEdgePos; } rule = ""; for (int i = resizedRowIndex + 1; i <= lastRowIndex; i++) { rule += "." + sheetId + " > div.rh.row" + i; if (lastRowIndex != i) { rule += ","; } } if (frozenRowHeaders != null && resizedRowIndex >= frozenRowHeaders.size()) { // need to add extra margin for the freeze pane for (int i = 1; i <= frozenRowHeaders.size(); i++) { margin += getRowHeight(i); } } margin += topOffset; if (frozenRowHeaders == null || resizedRowIndex > frozenRowHeaders.size()) { // only adjust outside freeze pane margin -= sheet.getScrollTop(); } if (!rule.isEmpty()) { rule += "{margin-top:" + margin + "px;}"; jsniUtil.insertRule(resizeStyle, rule); } rule = "." + sheetId + ".row-resizing > div.resize-line.rh {margin-top:" + (margin - 1) + "px;}"; jsniUtil.insertRule(resizeStyle, rule); showResizeTooltipRelativeTo(clientX, clientY); } private int getColHeaderSize() { if (colHeaders.isEmpty()) { return 0; } // some headers might be hidden, find one that isn't int index = 0; while (actionHandler.isColumnHidden(index + 1)) { index++; } MeasuredSize measuredSize = new MeasuredSize(); if (frozenColumnHeaders != null && frozenColumnHeaders.size() > 0 && index <= frozenColumnHeaders.size()) { measuredSize.measure(frozenColumnHeaders.get(index)); } else { measuredSize.measure(colHeaders.get(index)); } return (int) measuredSize.getOuterHeight(); } private int getRowHeaderSize() { if (rowHeaders.isEmpty()) { return 0; } // some headers might be hidden, find one that isn't int index = 0; while (actionHandler.isRowHidden(index + 1)) { index++; } MeasuredSize measuredSize = new MeasuredSize(); if (frozenRowHeaders != null && frozenRowHeaders.size() > 0 && index <= frozenRowHeaders.size()) { measuredSize.measure(frozenRowHeaders.get(index)); } else { measuredSize.measure(rowHeaders.get(index)); } return (int) measuredSize.getOuterWidth(); } private void handleColumnResizeDrag(int clientX, int clientY) { resized = true; int delta = clientX - resizeFirstEdgePos; if (delta < 0) { delta = 0; } jsniUtil.clearCSSRules(resizeStyle); // only the dragged header size has changed. if (delta > 0) { resizeTooltipLabel.setText("Width: " + delta + "px"); } else { resizeTooltipLabel.setText("Hide column"); } // enter custom size for the resized column header String rule = "." + sheetId + " > div.ch.col" + resizedColumnIndex + "{width:" + delta + "px;}"; jsniUtil.insertRule(resizeStyle, rule); int headersAfter = 0; int spaceAfter = sheet.getAbsoluteRight() - clientX; // might need to add more headers // count how many headers after resize one for (int i = resizedColumnIndex + 1; i <= lastColumnIndex && headersAfter < spaceAfter; i++) { headersAfter += actionHandler.getColWidthActual(i); } // adjust headers after resized one with margin int margin = clientX - resizeLastEdgePos; if (margin < resizeFirstEdgePos - resizeLastEdgePos) { margin = resizeFirstEdgePos - resizeLastEdgePos; } rule = ""; for (int i = resizedColumnIndex + 1; i <= lastColumnIndex; i++) { rule += "." + sheetId + " > div.ch.col" + i; if (lastColumnIndex != i) { rule += ","; } } if (frozenColumnHeaders != null && resizedColumnIndex >= frozenColumnHeaders.size()) { // need to add extra margin for the freeze pane for (int i = 1; i <= frozenColumnHeaders.size(); i++) { margin += actionHandler.getColWidthActual(i); } } margin = leftOffset + margin; if (frozenColumnHeaders == null || resizedColumnIndex > frozenColumnHeaders.size()) { // only adjust outside freeze pane margin -= sheet.getScrollLeft(); } if (!rule.isEmpty()) { rule += "{margin-left:" + margin + "px;}"; jsniUtil.insertRule(resizeStyle, rule); } rule = "." + sheetId + ".col-resizing > div.resize-line.ch {margin-left:" + (margin - 1) + "px;}"; jsniUtil.insertRule(resizeStyle, rule); showResizeTooltipRelativeTo(clientX, clientY); } private void showResizeTooltipRelativeTo(int clientX, int clientY) { int left = clientX + 10; int top = clientY - 25; resizeTooltip.setPopupPosition(left, top); } /** Replace stylesheet with the array of rules given */ private void resetStyleSheetRules(StyleElement stylesheet, List<String> rules) { jsniUtil.clearCSSRules(stylesheet); for (int i = 0; i < rules.size(); i++) { jsniUtil.insertRule(stylesheet, rules.get(i)); } } public CellData getCellData(int column, int row) { return cachedCellData.get(toKey(column, row)); } public String getCellValue(int column, int row) { CellData cd = getCellData(column, row); return cd == null ? "" : cd.value; } public boolean isCellLocked(int column, int row) { CellData cd = getCellData(column, row); return cd == null ? actionHandler.isColProtected(column) && actionHandler.isRowProtected(row) : cd.locked; } public String getCellFormulaValue(int column, int row) { CellData cd = getCellData(column, row); return cd == null ? "" : cd.formulaValue; } public String getOriginalCellValue(int column, int row) { CellData cd = getCellData(column, row); return cd == null ? "" : cd.originalValue; } private String createHeaderDNDHTML() { return HEADER_RESIZE_DND_HTML; } /** * Called after scrolling to move headers in order to keep them in sync with * the spreadsheet contents. Also effects the selection widget. */ private void moveHeadersToMatchScroll() { int negativeLeftMargin = 0 - sheet.getScrollLeft(); int negativeTopMargin = 0 - sheet.getScrollTop(); topRightPane.getStyle().setMarginLeft(negativeLeftMargin, Unit.PX); colGroupPane.getStyle().setMarginLeft(negativeLeftMargin, Unit.PX); bottomLeftPane.getStyle().setMarginTop(negativeTopMargin, Unit.PX); rowGroupPane.getStyle().setMarginTop(negativeTopMargin, Unit.PX); colGroupBorderPane.getStyle().setMarginLeft(negativeLeftMargin - calculatedRowGroupWidth, Unit.PX); rowGroupBorderPane.getStyle().setMarginTop(negativeTopMargin - calculatedColGroupHeight, Unit.PX); } private int calculateHeightForRows(int startIndex, int endIndex) { int top = 0; for (int i = startIndex; i <= endIndex; i++) { float rowHeight = actionHandler.getRowHeight(i); int rowHeightPX = convertPointsToPixel(rowHeight); top += rowHeightPX; definedRowHeights[i - 1] = rowHeightPX; } return top; } private int calculateWidthForColumns(int startIndex, int endIndex) { int left = 0; for (int i = startIndex; i <= endIndex; i++) { int colWidth = actionHandler.getColWidth(i); left += colWidth; } return left; } private String getRowDisplayString(int rowIndex) { return actionHandler.isRowHidden(rowIndex) ? "display:none;" : "display: flex;"; } private String getColumnDisplayString(int columnIndex) { return actionHandler.isColumnHidden(columnIndex) ? "display:none;" : ""; } private void resetRowAndColumnStyles() { final List<String> sizeStyleRules = new ArrayList<String>(); int initialTop = calculateTopValueOfScrolledRows(); int initialLeft = calculateLeftValueOfScrolledColumns(); createOverlayStyles(cellSizeAndPositionStyle, sizeStyleRules); createRowStyles(sizeStyleRules, firstRowIndex, lastRowIndex, initialTop); createColumnStyles(sizeStyleRules, firstColumnIndex, lastColumnIndex, initialLeft); // Create styles for freezed columns and rows if needed if (horizontalSplitPosition > 0) { createColumnStyles(sizeStyleRules, 1, horizontalSplitPosition, 0); } if (verticalSplitPosition > 0) { createRowStyles(sizeStyleRules, 1, verticalSplitPosition, 0); } resetStyleSheetRules(cellSizeAndPositionStyle, sizeStyleRules); } private void createOverlayStyles(StyleElement stylesheet, List<String> rules) { Set<String> overlayRowIndex = new HashSet<String>(); for (Entry<String, SheetOverlay> entry : sheetOverlays.entrySet()) { SheetOverlay overlay = entry.getValue(); overlayRowIndex.add("" + overlay.getRow()); } String[] overlaySelectors = new String[overlayRowIndex.size()]; overlayRowIndex.toArray(overlaySelectors); String[] overlayRules = jsniUtil.getOverlayRules(stylesheet, overlaySelectors); for (int i = 0; i < overlayRules.length; i++) { rules.add(overlayRules[i]); } } private int calculateLeftValueOfScrolledColumns() { int left = 0; for (int i = 1; i < (firstColumnIndex - horizontalSplitPosition); i++) { left += actionHandler.getColWidth(i); } return left; } private int calculateTopValueOfScrolledRows() { int top = 0; for (int i = 1; i < (firstRowIndex - verticalSplitPosition); i++) { top += definedRowHeights[i - 1]; } return top; } private void createRowStyles(List<String> rules, int startIndex, int endIndex, int initialTop) { int top = initialTop; Map<Integer, Integer> topMap = new HashMap<Integer, Integer>(); for (int i = startIndex; i <= endIndex; i++) { StringBuilder sb = new StringBuilder(); int rowHeightPX = definedRowHeights[i - 1]; sb.append(".").append(sheetId).append(" .sheet .row").append(i).append(", .").append(sheetId) .append(">.resize-line.row").append(i).append(" { ").append(getRowDisplayString(i)) .append("height: ").append(rowHeightPX).append("px; top:").append(top).append("px; }\n"); top += rowHeightPX; topMap.put(i, top); rules.add(sb.toString()); } // update merged cell top styles, otherwise they might reappear at the // end when their rows are no longer rendered for (Entry<Integer, MergedCell> entry : mergedCells.entrySet()) { int row = entry.getValue().getRow() - 1; if (!(row == endIndex && endIndex == verticalSplitPosition) && topMap.containsKey(row)) { entry.getValue().getElement().getStyle().setTop(topMap.get(row), Unit.PX); } else if (row < startIndex && endIndex != verticalSplitPosition) { entry.getValue().getElement().getStyle().setTop(0, Unit.PX); } } } private void createColumnStyles(List<String> rules, int startIndex, int endIndex, int initialLeft) { int left = initialLeft; Map<Integer, Integer> leftMap = new HashMap<Integer, Integer>(); for (int i = startIndex; i <= endIndex; i++) { StringBuilder sb = new StringBuilder(); int colWidth = actionHandler.getColWidth(i); sb.append(".").append(sheetId).append(" .sheet .col").append(i).append(", .").append(sheetId) .append(">.resize-line.col").append(i).append(" { ").append(getColumnDisplayString(i)) .append("width: ").append(colWidth).append("px; left:").append(left).append("px; }\n"); left += colWidth; leftMap.put(i, left); rules.add(sb.toString()); } // update merged cell left styles, otherwise they might reappear at the // beginning when their columns are no longer rendered int absoluteRight = getElement().getAbsoluteRight(); for (Entry<Integer, MergedCell> entry : mergedCells.entrySet()) { int col = entry.getValue().getCol() - 1; if (!(col == endIndex && endIndex == horizontalSplitPosition) && leftMap.containsKey(col)) { entry.getValue().getElement().getStyle().setLeft(leftMap.get(col), Unit.PX); } else if (col > endIndex && endIndex != horizontalSplitPosition) { entry.getValue().getElement().getStyle().setLeft(absoluteRight, Unit.PX); } } } private void updateSheetStyles() { // create row rules (height + top offset) definedRowHeights = new int[actionHandler.getMaxRows()]; topFrozenPanelHeight = 0; float topFrozenPanelHeightPx = 0; if (verticalSplitPosition > 0) { topFrozenPanelHeightPx = calculateHeightForRows(1, verticalSplitPosition); topFrozenPanelHeight = (int) (topFrozenPanelHeightPx + 1); } float bottomPanelHeightPx = calculateHeightForRows(verticalSplitPosition + 1, actionHandler.getMaxRows()); // create column rules (width + left offset) leftFrozenPanelWidth = 0; if (horizontalSplitPosition > 0) { leftFrozenPanelWidth = calculateWidthForColumns(1, horizontalSplitPosition); } int bottomPanelWidth = calculateWidthForColumns(horizontalSplitPosition + 1, actionHandler.getMaxColumns()); updateSheetPanePositions(); if (topFrozenPanelHeightPx > 0 && leftFrozenPanelWidth > 0) { topLeftPane.removeClassName(FREEZE_PANE_INACTIVE_STYLENAME); } else { topLeftPane.addClassName(FREEZE_PANE_INACTIVE_STYLENAME); } if (topFrozenPanelHeightPx > 0) { topRightPane.removeClassName(FREEZE_PANE_INACTIVE_STYLENAME); } else { topRightPane.addClassName(FREEZE_PANE_INACTIVE_STYLENAME); } if (leftFrozenPanelWidth > 0) { bottomLeftPane.removeClassName(FREEZE_PANE_INACTIVE_STYLENAME); } else { bottomLeftPane.addClassName(FREEZE_PANE_INACTIVE_STYLENAME); } // Styles for the header and selection widget location, scroll is faked // with margins. moveHeadersToMatchScroll handles updating. topRightPane.getStyle().setMarginLeft(0, Unit.PX); bottomLeftPane.getStyle().setMarginTop(0, Unit.PX); colGroupPane.getStyle().setMarginLeft(0, Unit.PX); rowGroupPane.getStyle().setMarginLeft(0, Unit.PX); moveHeadersToMatchScroll(); // update floater size the adjust scroll bars correctly floater.getStyle().setHeight(bottomPanelHeightPx, Unit.PX); floater.getStyle().setWidth(bottomPanelWidth, Unit.PX); // Update freeze pane styles bottomLeftPane.getStyle().setHeight(bottomPanelHeightPx, Unit.PX); topRightPane.getStyle().setWidth(bottomPanelWidth, Unit.PX); } /** * Updates the left & top style property for sheet panes depending if * headers are shown or not. */ void updateSheetPanePositions() { int extraSize = horizontalSplitPosition > 0 ? 1 : 0; if (spreadsheet.getAttribute("class").contains("report")) { extraSize = 0; } int widthIncrease = 0; if (rowHeaders != null && !rowHeaders.isEmpty()) { widthIncrease = getRowHeaderSize(); } int heightIncrease = 0; if (colHeaders != null && !colHeaders.isEmpty()) { heightIncrease = getColHeaderSize(); } // Measure formula bar height int formulaBarHeight = 0; if (actionHandler.getFormulaBarWidget() != null) { MeasuredSize measuredSize = new MeasuredSize(); measuredSize.measure(actionHandler.getFormulaBarWidget().getElement()); formulaBarHeight = (int) measuredSize.getOuterHeight(); } int addedHeaderHeight = updateExtraColumnHeaderElements(formulaBarHeight); int addedHeaderWidth = updateExtraRowHeaderElements(formulaBarHeight); updateExtraCornerElements(formulaBarHeight, addedHeaderHeight, addedHeaderWidth); if (!displayRowColHeadings) { widthIncrease = 0; heightIncrease = 0; } topOffset = heightIncrease + formulaBarHeight + addedHeaderHeight; leftOffset = widthIncrease + addedHeaderWidth; Style style = topLeftPane.getStyle(); style.setWidth(leftFrozenPanelWidth + widthIncrease + 1, Unit.PX); style.setHeight(topFrozenPanelHeight + heightIncrease, Unit.PX); style.setTop(formulaBarHeight + addedHeaderHeight, Unit.PX); style.setLeft(addedHeaderWidth, Unit.PX); style = topRightPane.getStyle(); // left offset is the same as the width increase style.setLeft(leftFrozenPanelWidth + leftOffset + extraSize, Unit.PX); style.setHeight(topFrozenPanelHeight + heightIncrease, Unit.PX); style.setTop(formulaBarHeight + addedHeaderHeight, Unit.PX); style = bottomLeftPane.getStyle(); // The +1 is to accommodate the vertical border of the freeze pane style.setWidth(leftFrozenPanelWidth + widthIncrease + 1, Unit.PX); style.setTop(topFrozenPanelHeight + topOffset, Unit.PX); style.setLeft(addedHeaderWidth, Unit.PX); style = sheet.getStyle(); style.setLeft(leftFrozenPanelWidth + leftOffset + extraSize, Unit.PX); style.setTop(topFrozenPanelHeight + topOffset, Unit.PX); style = corner.getStyle(); style.setTop(formulaBarHeight + addedHeaderHeight, Unit.PX); style.setLeft(addedHeaderWidth, Unit.PX); } private void updateConditionalFormattingStyles() { Map<Integer, String> styles = actionHandler.getConditionalFormattingStyles(); if (styles != null) { try { List<Integer> list = new ArrayList<Integer>(styles.keySet()); Collections.sort(list); final int listSize = list.size(); final StringBuilder sb = new StringBuilder(getRules(sheetStyle)); for (int i = 0; i < listSize; i++) { Integer key = list.get(i); String val = styles.get(key); sb.append(".v-spreadsheet." + sheetId + " .sheet .cell.cf" + key + " {" + val + "}"); } sheetStyle.removeAllChildren(); sheetStyle.appendChild(Document.get().createTextNode(sb.toString())); } catch (Exception e) { debugConsole.severe("SheetWidget:updateConditionalFormattingStyles: " + e.toString() + " while creating the cell styles"); } } } /** Clears the rules starting from the given index */ public native String getRules(StyleElement stylesheet) /*-{ var cssRules = stylesheet.sheet.cssRules? stylesheet.sheet.cssRules : stylesheet.sheet.rules; var rules = []; for (i=0; i<cssRules.length; i++){ rules.push(cssRules[i].cssText); } return rules.join(' '); }-*/; private void updateCellStyles() { boolean isDebugMode = ApplicationConfiguration.isDebugMode(); // styles for individual cells Map<Integer, String> styles = actionHandler.getCellStyleToCSSStyle(); long started = 0; if (isDebugMode) { started = System.currentTimeMillis(); } Map<Integer, Integer> rowIndexToStyleIndex = actionHandler.getRowIndexToStyleIndex(); Map<Integer, Integer> columnIndexToStyleIndex = actionHandler.getColumnIndexToStyleIndex(); if (styles != null) { try { final StringBuilder sb = new StringBuilder(getRules(sheetStyle)); for (Entry<Integer, String> entry : styles.entrySet()) { if (entry.getKey() == 0) { sb.append(".v-spreadsheet." + sheetId + " .sheet .cell {" + entry.getValue() + "}"); } else { sb.append( getSelectorsForStyle(entry.getKey(), rowIndexToStyleIndex, columnIndexToStyleIndex) + " {" + entry.getValue() + "}"); } } sheetStyle.removeAllChildren(); sheetStyle.appendChild(Document.get().createTextNode(sb.toString())); } catch (Exception e) { debugConsole .severe("SheetWidget:updateStyles: " + e.toString() + " while creating the cell styles"); } } if (isDebugMode) { long ended = System.currentTimeMillis(); debugConsole.info("Style update took:" + (ended - started) + "ms"); } recalculateCellStyleWidthValues(); createCellRangeRule(); } private String getSelectorsForStyle(Integer index, Map<Integer, Integer> rowIndexToStyleIndex, Map<Integer, Integer> columnIndexToStyleIndex) { StringBuilder sb = new StringBuilder(".v-spreadsheet."); sb.append(sheetId).append(" .sheet .cell.cs").append(index); for (Map.Entry<Integer, Integer> entry : rowIndexToStyleIndex.entrySet()) { if (entry.getValue() == index) { sb.append(", .v-spreadsheet.").append(sheetId).append(" .sheet .row").append(entry.getKey()) .append(".cell.cs0"); } } for (Map.Entry<Integer, Integer> entry : columnIndexToStyleIndex.entrySet()) { if (entry.getValue() == index) { sb.append(", .v-spreadsheet.").append(sheetId).append(" .sheet .col").append(entry.getKey()) .append(".cell.cs0"); } } return sb.toString(); } private void createCellRangeRule() { DivElement tempDiv = Document.get().createDivElement(); tempDiv.addClassName("cell-range-bg-color"); tempDiv.getStyle().setWidth(0, Unit.PX); tempDiv.getStyle().setHeight(0, Unit.PX); sheet.appendChild(tempDiv); ComputedStyle cs = new ComputedStyle(tempDiv); String bgCol = cs.getProperty("backgroundColor"); bgCol = bgCol.replace("!important", ""); sheet.removeChild(tempDiv); if (bgCol != null && !bgCol.trim().isEmpty()) { Canvas c = Canvas.createIfSupported(); c.setCoordinateSpaceHeight(1); c.setCoordinateSpaceWidth(1); c.getContext2d().setFillStyle(bgCol); c.getContext2d().fillRect(0, 0, 1, 1); String bgImage = "url(\"" + c.toDataUrl() + "\")"; jsniUtil.insertRule(sheetStyle, "." + sheetId + " .sheet .cell.cell-range {" + "background-image: " + bgImage + " !important;" + "}"); } else { // Fall back to the default color jsniUtil.insertRule(sheetStyle, "." + sheetId + " .sheet .cell.cell-range {" + "background-color: rgba(232, 242, 252, 0.8) !important;" + "}"); } } /** * Recalculates the width needed for each cell style for showing numbers. */ private void recalculateCellStyleWidthValues() { Set<Integer> keys = actionHandler.getCellStyleToCSSStyle().keySet(); HashMap<Integer, Float> cellStyleWidthRatioMap = new HashMap<Integer, Float>(); sheet.appendChild(fontWidthDummyElement); fontWidthDummyElement.setInnerText("5555555555"); for (Integer key : keys) { fontWidthDummyElement.setClassName("cell cs" + key); int clientWidth = fontWidthDummyElement.getClientWidth(); cellStyleWidthRatioMap.put(key, new BigDecimal(clientWidth).divide(new BigDecimal(10)).floatValue()); } fontWidthDummyElement.removeFromParent(); actionHandler.setCellStyleWidthRatios(cellStyleWidthRatioMap); } int measureValueWidth(String cellStyle, String value) { sheet.appendChild(fontWidthDummyElement); fontWidthDummyElement.setClassName("cell " + cellStyle); fontWidthDummyElement.setInnerText(value); int clientWidth = fontWidthDummyElement.getClientWidth(); fontWidthDummyElement.removeFromParent(); return clientWidth; } private void removeFrozenHeaders(ArrayList<DivElement> headers) { for (DivElement e : headers) { e.removeFromParent(); } headers.clear(); } private void resetFrozenColumnHeaders() { if (horizontalSplitPosition < frozenColumnHeaders.size()) { // remove extra while (frozenColumnHeaders.size() > horizontalSplitPosition) { frozenColumnHeaders.remove(frozenColumnHeaders.size() - 1).removeFromParent(); } } else { // add as many as needed for (int i = frozenColumnHeaders.size() + 1; i <= horizontalSplitPosition; i++) { DivElement colHeader = Document.get().createDivElement(); colHeader.setInnerHTML(actionHandler.getColHeader(i) + createHeaderDNDHTML()); colHeader.setClassName("ch col" + (i)); frozenColumnHeaders.add(colHeader); topLeftPane.appendChild(colHeader); } } } private void resetFrozenRowHeaders() { if (verticalSplitPosition < frozenRowHeaders.size()) { // remove extra while (frozenRowHeaders.size() > verticalSplitPosition) { frozenRowHeaders.remove(frozenRowHeaders.size() - 1).removeFromParent(); } } else { // add as many as needed for (int i = frozenRowHeaders.size() + 1; i <= verticalSplitPosition; i++) { DivElement rowHeader = Document.get().createDivElement(); rowHeader.setInnerHTML(actionHandler.getRowHeader(i) + createHeaderDNDHTML()); rowHeader.setClassName("rh row" + (i)); frozenRowHeaders.add(rowHeader); topLeftPane.appendChild(rowHeader); } } } /** * Update the column headers to match the state. Create and recycle header * divs as needed. */ private void resetColHeaders() { if (frozenColumnHeaders != null) { if (horizontalSplitPosition > 0) { resetFrozenColumnHeaders(); } else { removeFrozenHeaders(frozenColumnHeaders); frozenColumnHeaders = null; } } else if (horizontalSplitPosition > 0) { frozenColumnHeaders = new ArrayList<DivElement>(); resetFrozenColumnHeaders(); } for (int i = firstColumnIndex; i <= lastColumnIndex; i++) { if (i > horizontalSplitPosition) { DivElement colHeader; if (i - firstColumnIndex < colHeaders.size()) { colHeader = colHeaders.get(i - firstColumnIndex); } else { colHeader = Document.get().createDivElement(); topRightPane.appendChild(colHeader); colHeaders.add(i - firstColumnIndex, colHeader); } colHeader.setClassName("ch col" + (i)); colHeader.setInnerHTML(actionHandler.getColHeader(i) + createHeaderDNDHTML()); if (selectedColHeaderIndexes.contains(i)) { colHeader.addClassName(SELECTED_COLUMN_HEADER_CLASSNAME); } } else { debugConsole.severe("Trying to add plain column header (index:" + i + ") into frozen pane, horizontalSplitPosition: " + horizontalSplitPosition); } } while (colHeaders.size() > (lastColumnIndex - firstColumnIndex + 1)) { colHeaders.remove(colHeaders.size() - 1).removeFromParent(); } } /** * Update the row headers to match the state. Create and recycle header divs * as needed. */ private void resetRowHeaders() { if (frozenRowHeaders != null) { if (verticalSplitPosition > 0) { resetFrozenRowHeaders(); } else { removeFrozenHeaders(frozenRowHeaders); frozenRowHeaders = null; } } else if (verticalSplitPosition > 0) { frozenRowHeaders = new ArrayList<DivElement>(); resetFrozenRowHeaders(); } for (int i = firstRowIndex; i <= lastRowIndex; i++) { if (verticalSplitPosition < i) { DivElement rowHeader; if (i - firstRowIndex < rowHeaders.size()) { rowHeader = rowHeaders.get(i - firstRowIndex); } else { rowHeader = Document.get().createDivElement(); bottomLeftPane.appendChild(rowHeader); rowHeaders.add(i - firstRowIndex, rowHeader); } rowHeader.setClassName("rh row" + (i)); rowHeader.setInnerHTML(actionHandler.getRowHeader(i) + createHeaderDNDHTML()); if (selectedRowHeaderIndexes.contains(i)) { rowHeader.addClassName(SELECTED_ROW_HEADER_CLASSNAME); } } else { debugConsole.severe("Trying to add plain row header (index:" + i + ") into frozen pane, verticalSplitPosition: " + verticalSplitPosition); } } // Remove unused headers while (rowHeaders.size() > (lastRowIndex - firstRowIndex + 1)) { rowHeaders.remove(rowHeaders.size() - 1).removeFromParent(); } } private void resetScrollView(int scrollLeft, int scrollTop) { sheet.setScrollLeft(scrollLeft); sheet.setScrollTop(scrollTop); scrollViewHeight = sheet.getOffsetHeight(); scrollViewWidth = sheet.getOffsetWidth(); previousScrollLeft = scrollLeft; previousScrollTop = scrollTop; firstRowIndex = 1; firstRowPosition = 0; if (verticalSplitPosition > 0) { firstRowIndex = verticalSplitPosition + 1; } firstColumnIndex = 1; firstColumnPosition = 0; if (horizontalSplitPosition > 0) { firstColumnIndex = horizontalSplitPosition + 1; } lastColumnIndex = 0; clearSelectedCellStyle(); clearCellRangeStyles(); // move the indexes to the correct scroll position int columnBufferSize = actionHandler.getColumnBufferSize(); if (firstColumnPosition < (scrollLeft - columnBufferSize)) { do { firstColumnPosition += actionHandler.getColWidthActual(firstColumnIndex); firstColumnIndex++; } while (firstColumnPosition < (scrollLeft - columnBufferSize)); } lastColumnIndex = firstColumnIndex; lastColumnPosition = firstColumnPosition + actionHandler.getColWidthActual(firstColumnIndex); int rowBufferSize = actionHandler.getRowBufferSize(); if (firstRowPosition < (scrollTop - rowBufferSize)) { do { if (firstRowIndex >= actionHandler.getDefinedRows()) { firstRowPosition += getDefaultRowHeight(); } else { // firstRowPosition += convertPointsToPixel(actionHandler // .getRowHeight(firstRowIndex)); firstRowPosition += getRowHeight(firstRowIndex); } firstRowIndex++; } while (firstRowPosition < (scrollTop - rowBufferSize)); } lastRowIndex = firstRowIndex; lastRowPosition = firstRowPosition + getRowHeight(lastRowIndex); // count how many columns fit to view on first view while (lastColumnPosition < (scrollLeft + scrollViewWidth + columnBufferSize) && lastColumnIndex < actionHandler.getMaxColumns()) { lastColumnIndex++; lastColumnPosition += actionHandler.getColWidthActual(lastColumnIndex); } // count how many rows should be displayed while (lastRowPosition < (scrollTop + scrollViewHeight + rowBufferSize) && lastRowIndex < actionHandler.getMaxRows()) { lastRowIndex++; if (lastRowIndex >= actionHandler.getDefinedRows()) { lastRowPosition += getDefaultRowHeight(); } else { lastRowPosition += getRowHeight(lastRowIndex); } } } private void resetCellContents() { clearListOfCells(topLeftCells); for (ArrayList<Cell> row : topRightRows) { clearListOfCells(row); } topRightRows.clear(); for (ArrayList<Cell> row : bottomLeftRows) { clearListOfCells(row); } bottomLeftRows.clear(); // Remove old cells for (ArrayList<Cell> row : rows) { clearListOfCells(row); } rows.clear(); sheet.appendChild(floater); // create freeze panes' cells if (verticalSplitPosition > 0 && horizontalSplitPosition > 0) { createTopLeftPaneCells(); createTopRightPaneCells(); createBottomLeftPaneCells(); } else if (verticalSplitPosition > 0) { createTopRightPaneCells(); } else if (horizontalSplitPosition > 0) { createBottomLeftPaneCells(); } for (int i = firstRowIndex; i <= lastRowIndex; i++) { ArrayList<Cell> row = new ArrayList<Cell>(lastColumnIndex); for (int j = firstColumnIndex; j <= lastColumnIndex; j++) { Cell cell = new Cell(this, j, i); sheet.appendChild(cell.getElement()); row.add(cell); } rows.add(row); } } private void createBottomLeftPaneCells() { for (int v = verticalSplitPosition > 0 ? verticalSplitPosition + 1 : 1; v <= lastRowIndex; v++) { ArrayList<Cell> row = new ArrayList<Cell>(); for (int h = 1; h <= horizontalSplitPosition; h++) { Cell cell = new Cell(this, h, v); bottomLeftPane.appendChild(cell.getElement()); row.add(cell); } bottomLeftRows.add(row); } } private void createTopRightPaneCells() { for (int v = 1; v <= verticalSplitPosition; v++) { ArrayList<Cell> row = new ArrayList<Cell>(); for (int h = horizontalSplitPosition > 0 ? horizontalSplitPosition + 1 : 1; h <= lastColumnIndex; h++) { Cell cell = new Cell(this, h, v); topRightPane.appendChild(cell.getElement()); row.add(cell); } topRightRows.add(row); } } private void createTopLeftPaneCells() { for (int v = 1; v <= verticalSplitPosition; v++) { for (int h = 1; h <= horizontalSplitPosition; h++) { Cell cell = new Cell(this, h, v); topLeftPane.appendChild(cell.getElement()); topLeftCells.add(cell); } } } private void clearListOfCells(ArrayList<Cell> row) { for (Cell cell : row) { cell.getElement().removeFromParent(); } row.clear(); } /** * Update the headers and cells in the spreadsheet to reflect the current * view area. Runs the escalator (if needed) and requests cell data from * handler (if needed). */ private void onSheetScroll() { int scrollTop = sheet.getScrollTop(); int scrollLeft = sheet.getScrollLeft(); int vScrollDiff = scrollTop - previousScrollTop; int hScrollDiff = scrollLeft - previousScrollLeft; if (Math.abs(vScrollDiff) < (actionHandler.getRowBufferSize() / 2) && Math.abs(hScrollDiff) < (actionHandler.getColumnBufferSize() / 2)) { return; } try { if (Math.abs(hScrollDiff) > (actionHandler.getColumnBufferSize() / 2)) { previousScrollLeft = scrollLeft; if (hScrollDiff > 0) { handleHorizontalScrollRight(scrollLeft); } else if (hScrollDiff < 0) { handleHorizontalScrollLeft(scrollLeft); } } if (Math.abs(vScrollDiff) > (actionHandler.getRowBufferSize() / 2)) { previousScrollTop = scrollTop; if (vScrollDiff > 0) { handleVerticalScrollDown(scrollTop); } else if (vScrollDiff < 0) { handleVerticalScrollUp(scrollTop); } } requester.trigger(); } catch (Throwable t) { debugConsole.severe("SheetWidget:updateSheetDisplay: " + t.toString()); } // update cells resetRowAndColumnStyles(); updateCells(vScrollDiff, hScrollDiff); ensureCellSelectionStyles(); } private void ensureCellSelectionStyles() { for (CellCoord coord : cellRangeStyledCoords) { if (coord.getCol() != selectedCellCol || coord.getRow() != selectedCellRow) { Cell cell = getCell(coord.getCol(), coord.getRow()); if (cell != null) { cell.getElement().addClassName(CELL_RANGE_CLASSNAME); cellRangeStyledCells.add(cell); } Cell mergedCell = getMergedCell(toKey(coord.getCol(), coord.getRow())); if (mergedCell != null) { cellRangeStyledCells.add(mergedCell); mergedCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } } } if (highlightedCellCoord != null) { Cell cell = getCell(highlightedCellCoord.getCol(), highlightedCellCoord.getRow()); if (cell != null) { cell.getElement().addClassName(CELL_SELECTION_CLASSNAME); } } actionHandler.getFormulaBarWidget().ensureSelectionStylesAfterScroll(); } private void runEscalatorOnAllCells(int r1, int r2, int c1, int c2, ArrayList<ArrayList<Cell>> rows, Element paneElement) { // run escalator on all rows&columns, remove if necessary for (int r = r1; r <= r2; r++) { final ArrayList<Cell> row; // run escalator vertically if (rows.size() > (r - r1)) { row = rows.get(r - r1); } else { row = new ArrayList<Cell>(); row.ensureCapacity(c2 - c1 + 1); rows.add(r - r1, row); } // run escalator horizontally: for (int c = c1; c <= c2; c++) { final Cell cell; if (row.size() > (c - c1)) { cell = row.get(c - c1); cell.update(c, r, getCellData(c, r)); } else { cell = new Cell(this, c, r, getCellData(c, r)); paneElement.appendChild(cell.getElement()); row.add(c - c1, cell); } } while (row.size() > (c2 - c1 + 1)) { row.remove(row.size() - 1).getElement().removeFromParent(); } } while (rows.size() > r2 - r1 + 1) { for (Cell cell : rows.remove(rows.size() - 1)) { cell.getElement().removeFromParent(); } } updateOverflows(false); } private void runEscalatorPartially(int vScrollDiff, int hScrollDiff, int r1, int r2, int c1, int c2, ArrayList<ArrayList<Cell>> rows, Element paneElement) { int firstR = rows.get(0).get(0).getRow(); int lastR = rows.get(rows.size() - 1).get(0).getRow(); int firstC = rows.get(0).get(0).getCol(); int lastC = rows.get(0).get(rows.get(0).size() - 1).getCol(); // run escalator on some rows/cells ArrayList<ArrayList<Cell>> tempRows = new ArrayList<ArrayList<Cell>>(); for (Iterator<ArrayList<Cell>> iterator = rows.iterator(); iterator.hasNext();) { final ArrayList<Cell> row = iterator.next(); int rIndex = row.get(0).getRow(); // FIND OUT IF ROW INDEX HAS CHANGED FOR THE ROW (swap/remove) // scroll down if (vScrollDiff > 0) { if (rIndex < r1) { // swap or remove if (lastR < r2) { // swap row to bottom rIndex = ++lastR; iterator.remove(); tempRows.add(row); } else { // remove row for (Cell cell : row) { cell.getElement().removeFromParent(); } iterator.remove(); continue; } } } // scroll up else if (vScrollDiff < 0) { // swap or remove if (rIndex > r2) { if (firstR > r1) { // swap from bottom to top rIndex = --firstR; iterator.remove(); tempRows.add(row); } else { // remove row for (Cell cell : row) { cell.getElement().removeFromParent(); } iterator.remove(); continue; } } } firstC = row.get(0).getCol(); lastC = row.get(row.size() - 1).getCol(); final ArrayList<Cell> tempCols = new ArrayList<Cell>(); for (Iterator<Cell> cells = row.iterator(); cells.hasNext();) { Cell cell = cells.next(); int cIndex = cell.getCol(); // scroll right if (hScrollDiff > 0) { // move cells from left to right if (cIndex < c1) { // swap or remove if (lastC < c2) { // swap cell to right cIndex = ++lastC; cells.remove(); tempCols.add(cell); } else { // remove cell cell.getElement().removeFromParent(); cells.remove(); continue; } } } else if (hScrollDiff < 0) { // scroll left // move cells from right to left if (cIndex > c2) { // swap or remove if (firstC > c1) { // swap cell to right cIndex = --firstC; cells.remove(); tempCols.add(cell); } else { // remove cell cell.getElement().removeFromParent(); cells.remove(); continue; } } } if (cIndex != cell.getCol() || rIndex != cell.getRow()) { cell.update(cIndex, rIndex, getCellData(cIndex, rIndex)); } } if (hScrollDiff > 0) { // add moved cells to collection for (Cell cell : tempCols) { row.add(cell); } // add new cells if required while (lastC < c2) { lastC++; Cell cell = new Cell(this, lastC, rIndex, getCellData(lastC, rIndex)); paneElement.appendChild(cell.getElement()); row.add(cell); } } else if (hScrollDiff < 0) { // add moved cells to collection for (Cell cell : tempCols) { row.add(0, cell); } // add new cells if required while (firstC > c1) { firstC--; Cell cell = new Cell(this, firstC, rIndex, getCellData(firstC, rIndex)); paneElement.appendChild(cell.getElement()); row.add(0, cell); } } } // add moved rows to collection if (vScrollDiff > 0) { for (ArrayList<Cell> row : tempRows) { rows.add(row); } } else { for (ArrayList<Cell> row : tempRows) { rows.add(0, row); } } // add new rows if necessary if (vScrollDiff > 0) { while (lastR < r2) { ArrayList<Cell> row = new ArrayList<Cell>(c2 - c1 + 1); lastR++; for (int i = c1; i <= c2; i++) { Cell cell = new Cell(this, i, lastR, getCellData(i, lastR)); row.add(cell); paneElement.appendChild(cell.getElement()); } rows.add(row); } } else if (vScrollDiff < 0) { while (firstR > r1) { ArrayList<Cell> row = new ArrayList<Cell>(); row.ensureCapacity(c2 - c1 + 1); firstR--; for (int i = c1; i <= c2; i++) { Cell cell = new Cell(this, i, firstR, getCellData(i, firstR)); row.add(cell); paneElement.appendChild(cell.getElement()); } rows.add(0, row); } } updateOverflows(false); } /** push the cells to the escalator */ private void updateCells(int vScrollDiff, int hScrollDiff) { Cell firstCell = rows.get(0).get(0); ArrayList<Cell> lastRow = rows.get(rows.size() - 1); Cell lastCell = lastRow.get(lastRow.size() - 1); int firstR = firstCell.getRow(); int lastR = lastCell.getRow(); int firstC = firstCell.getCol(); int lastC = lastCell.getCol(); // orphan custom editor if visible (it is outside of visible range, and // cell may be re-purposed) removeCustomCellEditor(); if (firstR > lastRowIndex || lastR < firstRowIndex || firstC > lastColumnIndex || lastC < firstColumnIndex) { // big scroll runEscalatorOnAllCells(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex, rows, sheet); if (vScrollDiff != 0 && horizontalSplitPosition > 0) { runEscalatorOnAllCells(firstRowIndex, lastRowIndex, 1, horizontalSplitPosition, bottomLeftRows, bottomLeftPane); } if (hScrollDiff != 0 && verticalSplitPosition > 0) { runEscalatorOnAllCells(1, verticalSplitPosition, firstColumnIndex, lastColumnIndex, topRightRows, topRightPane); } } else { runEscalatorPartially(vScrollDiff, hScrollDiff, firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex, rows, sheet); if (vScrollDiff != 0 && horizontalSplitPosition > 0) { runEscalatorPartially(vScrollDiff, 0, firstRowIndex, lastRowIndex, 1, horizontalSplitPosition, bottomLeftRows, bottomLeftPane); } if (hScrollDiff != 0 && verticalSplitPosition > 0) { runEscalatorPartially(0, hScrollDiff, 1, verticalSplitPosition, firstColumnIndex, lastColumnIndex, topRightRows, topRightPane); } } } private void handleHorizontalScrollLeft(int scrollLeft) { int columnBufferSize = actionHandler.getColumnBufferSize(); int leftBound = scrollLeft - columnBufferSize; int rightBound = scrollLeft + scrollViewWidth + columnBufferSize; if (leftBound < 0) { leftBound = 0; } int maxFirstColumn = horizontalSplitPosition + 1; // hSP is 0 when no while (firstColumnPosition > leftBound && firstColumnIndex > maxFirstColumn) { if (lastColumnPosition - actionHandler.getColWidthActual(lastColumnIndex) > rightBound) { lastColumnPosition -= actionHandler.getColWidthActual(lastColumnIndex); lastColumnIndex--; } firstColumnIndex--; firstColumnPosition -= actionHandler.getColWidthActual(firstColumnIndex); } if (firstColumnPosition <= 0 || firstColumnIndex <= 1) { firstColumnPosition = 0; firstColumnIndex = maxFirstColumn; } while (rightBound < (lastColumnPosition - actionHandler.getColWidthActual(lastColumnIndex)) && lastColumnIndex > 1) { lastColumnPosition -= actionHandler.getColWidthActual(lastColumnIndex); lastColumnIndex--; } resetColHeaders(); } /** * Calculates viewed cells after a scroll to right. Runs the escalator for * column headers. * * @param scrollLeft */ private void handleHorizontalScrollRight(int scrollLeft) { int columnBufferSize = actionHandler.getColumnBufferSize(); int leftBound = scrollLeft - columnBufferSize; int rightBound = scrollLeft + scrollViewWidth + columnBufferSize; if (leftBound < 0) { leftBound = 0; } final int maximumCols = actionHandler.getMaxColumns(); while (lastColumnPosition < rightBound && lastColumnIndex < maximumCols) { if ((firstColumnPosition + actionHandler.getColWidthActual(firstColumnIndex)) < leftBound) { firstColumnPosition += actionHandler.getColWidthActual(firstColumnIndex); firstColumnIndex++; } lastColumnIndex++; lastColumnPosition += actionHandler.getColWidthActual(lastColumnIndex); } while (leftBound > (firstColumnPosition + actionHandler.getColWidthActual(firstColumnIndex)) && firstColumnIndex < maximumCols) { firstColumnPosition += actionHandler.getColWidthActual(firstColumnIndex); firstColumnIndex++; } resetColHeaders(); } private void handleVerticalScrollDown(int scrollTop) { int rowBufferSize = actionHandler.getRowBufferSize(); int topBound = scrollTop - rowBufferSize; int bottomBound = scrollTop + scrollViewHeight + rowBufferSize; if (topBound < 0) { topBound = 0; } final int maximumRows = actionHandler.getMaxRows(); while (lastRowPosition < bottomBound && lastRowIndex < maximumRows) { if ((firstRowPosition + getRowHeight(firstRowIndex)) < topBound) { firstRowPosition += getRowHeight(firstRowIndex); firstRowIndex++; } lastRowIndex++; lastRowPosition += getRowHeight(lastRowIndex); } while (topBound > (firstRowPosition + getRowHeight(firstRowIndex)) && firstRowIndex < maximumRows) { firstRowPosition += getRowHeight(firstRowIndex); firstRowIndex++; } resetRowHeaders(); } private void handleVerticalScrollUp(int scrollTop) { int rowBufferSize = actionHandler.getRowBufferSize(); int topBound = scrollTop - rowBufferSize; int bottomBound = scrollTop + scrollViewHeight + rowBufferSize; if (topBound < 0) { topBound = 0; } int maxTopRow = verticalSplitPosition + 1; // vSP is 0 when no split while (firstRowPosition > topBound && firstRowIndex > maxTopRow) { if ((lastRowPosition - getRowHeight(lastRowIndex)) > bottomBound) { lastRowPosition -= getRowHeight(lastRowIndex); lastRowIndex--; } firstRowIndex--; firstRowPosition -= getRowHeight(firstRowIndex); } if (firstRowPosition <= 0 || firstRowIndex <= 1) { firstRowPosition = 0; firstRowIndex = maxTopRow; } while (bottomBound < (lastRowPosition - getRowHeight(lastRowIndex)) && lastRowIndex > 1) { lastRowPosition -= getRowHeight(lastRowIndex); lastRowIndex--; } resetRowHeaders(); } public TextBox getInlineEditor() { // FIXME setter for operations instead? return input; } public boolean isSelectedCellCustomized() { return customWidgetMap != null && customWidgetMap.containsKey(getSelectedCellKey()); } public void showCustomWidgets(HashMap<String, Widget> newWidgetMap) { if (customWidgetMap != null) { for (Widget w : customWidgetMap.values()) { if (!newWidgetMap.values().contains(w)) { w.removeFromParent(); } } } if (verticalSplitPosition > 0 && horizontalSplitPosition > 0) { // top left pane showRegionWidgets(newWidgetMap, 1, verticalSplitPosition, 1, horizontalSplitPosition); } if (verticalSplitPosition > 0) { // top right pane showRegionWidgets(newWidgetMap, 1, verticalSplitPosition, firstColumnIndex, lastColumnIndex); } if (horizontalSplitPosition > 0) { // bottom left pane showRegionWidgets(newWidgetMap, 1, firstRowIndex, lastRowIndex, horizontalSplitPosition); } showRegionWidgets(newWidgetMap, firstColumnIndex, lastColumnIndex, firstRowIndex, lastRowIndex); customWidgetMap = newWidgetMap; } private void showRegionWidgets(HashMap<String, Widget> newWidgets, int col1, int col2, int row1, int row2) { for (int r = row1; r <= row2; r++) { for (int c = col1; c <= col2; c++) { final String key = toKey(c, r); if (newWidgets.containsKey(key)) { Cell cell; if (isMergedCell(key)) { cell = getMergedCell(key); } else { cell = getCell(c, r); } Widget customWidget = newWidgets.get(key); addCustomWidgetToCell(cell, customWidget); } } } } private void addCustomWidgetToCell(Cell cell, Widget customWidget) { cell.setValue(null); Widget parent = customWidget.getParent(); if (parent != null) { if (equals(parent)) { cell.getElement().appendChild(customWidget.getElement()); } else { customWidget.removeFromParent(); cell.getElement().appendChild(customWidget.getElement()); adopt(customWidget); } } else { cell.getElement().appendChild(customWidget.getElement()); adopt(customWidget); } } public void addSheetOverlay(String key, SheetOverlay overlay) { boolean inTop = verticalSplitPosition >= overlay.getRow(); boolean inLeft = horizontalSplitPosition >= overlay.getCol(); if (inTop && inLeft) { topLeftPane.appendChild(overlay.getElement()); } else if (inTop) { topRightPane.appendChild(overlay.getElement()); } else if (inLeft) { bottomLeftPane.appendChild(overlay.getElement()); } else { sheet.appendChild(overlay.getElement()); } adopt(overlay); sheetOverlays.put(key, overlay); } public void updateOverlayInfo(String key, OverlayInfo overlayInfo) { sheetOverlays.get(key).updateSizeLocationPadding(overlayInfo); } public void removeSheetOverlay(String key) { SheetOverlay overlay = sheetOverlays.remove(key); // because of a bug in SpreadsheetConnector, this method sometimes // called too late, when the overlays were already removed (after // switching a tab) if (overlay != null) { remove(overlay); } } public void addMergedRegion(MergedRegion region) { StringBuilder sb = new StringBuilder(); for (int r = region.row1; r <= region.row2; r++) { for (int c = region.col1; c <= region.col2; c++) { sb.append(toCssKey(c, r)); if (r != region.row2 || c != region.col2) { sb.append(","); } } } if (sb.length() != 0) { sb.append(MERGED_REGION_CELL_STYLE); jsniUtil.insertRule(mergedRegionStyle, sb.toString()); } String key = toKey(region.col1, region.row1); MergedCell mergedCell = new MergedCell(this, region.col1, region.row1); String cellStyle = "cs0"; Cell cell = getCell(region.col1, region.row1); if (cell != null) { cellStyle = cell.getCellStyle(); } mergedCell.setValue(getCellValue(region.col1, region.row1), cellStyle, false); DivElement element = mergedCell.getElement(); element.addClassName(MERGED_CELL_CLASSNAME); updateMergedRegionRegionSize(region, mergedCell); getPaneElementForCell(region.col1, region.row1).appendChild(element); mergedCells.put(region.id, mergedCell); // need to update the possible cell comment for the merged cell if (cellHasComment(key)) { mergedCell.showCellCommentMark(); } if (cellHasInvalidFormula(key)) { mergedCell.showInvalidFormulaIndicator(); } if (alwaysVisibleCellComments.containsKey(key)) { CellComment cellComment = alwaysVisibleCellComments.get(key); cellComment.showDependingToCellRightCorner((Element) element.cast(), region.row1, region.col1); } // need to update the possible custom widget for the merged cell if (customWidgetMap != null && customWidgetMap.containsKey(key)) { Widget customWidget = customWidgetMap.get(key); addCustomWidgetToCell(mergedCell, customWidget); } } /** * For internal use only! May be removed in the future. */ void checkMergedRegionPositions() { int initialLeft = calculateLeftValueOfScrolledColumns(); createColumnStyles(new ArrayList<String>(), firstColumnIndex, lastColumnIndex, initialLeft); } private void updateOverflownMergedCellSizes() { for (Entry<MergedRegion, Cell> entry : overflownMergedCells.entrySet()) { recalculateOverflownMergedCellHeight(entry.getKey(), entry.getValue()); recalculateOverflownMergedCellWidth(entry.getKey(), entry.getValue()); } } private void recalculateOverflownMergedCellWidth(MergedRegion region, Cell cell) { if (region.col1 <= horizontalSplitPosition && region.col2 > horizontalSplitPosition) { int[] colWidths = actionHandler.getColWidths(); int width = selectionWidget.countSum(colWidths, region.col1, horizontalSplitPosition + 1); int extraWidth = selectionWidget.countSum(colWidths, horizontalSplitPosition + 1, region.col2 + 1) - sheet.getScrollLeft() + 1; if (extraWidth > 0) { width += extraWidth; cell.getElement().getStyle().clearProperty("borderRight"); } else { cell.getElement().getStyle().setProperty("borderRight", "0"); } cell.getElement().getStyle().setWidth(width, Unit.PX); } } private void recalculateOverflownMergedCellHeight(MergedRegion region, Cell cell) { if (region.row1 <= verticalSplitPosition && region.row2 > verticalSplitPosition) { int[] rowHeights = actionHandler.getRowHeightsPX(); int height = selectionWidget.countSum(rowHeights, region.row1, verticalSplitPosition + 1); int extraHeight = selectionWidget.countSum(rowHeights, verticalSplitPosition + 1, region.row2 + 1) + 1 - sheet.getScrollTop(); if (extraHeight > 0) { height += extraHeight; cell.getElement().getStyle().clearProperty("borderBottom"); } else { cell.getElement().getStyle().setProperty("borderBottom", "0"); } cell.getElement().getStyle().setHeight(height, Unit.PX); } } private Element getPaneElementForCell(int col1, int row1) { if (col1 <= horizontalSplitPosition) { if (row1 <= verticalSplitPosition) { return topLeftPane; } else { return bottomLeftPane; } } else if (row1 <= verticalSplitPosition) { if (col1 <= horizontalSplitPosition) { return topLeftPane; } else { return topRightPane; } } return sheet; } private void updateMergedRegionRegionSize(MergedRegion region, Cell mergedCell) { int width = 0; int height = 0; DivElement element = mergedCell.getElement(); if (horizontalSplitPosition >= region.col1 && region.col2 > horizontalSplitPosition) { recalculateOverflownMergedCellWidth(region, mergedCell); overflownMergedCells.put(region, mergedCell); width = 1; } else { width = selectionWidget.countSum(actionHandler.getColWidths(), region.col1, region.col2 + 1); element.getStyle().setWidth(width, Unit.PX); } if (verticalSplitPosition >= region.row1 && region.row2 > verticalSplitPosition) { recalculateOverflownMergedCellHeight(region, mergedCell); overflownMergedCells.put(region, mergedCell); height = 1; } else { height = selectionWidget.countSum(actionHandler.getRowHeightsPX(), region.row1, region.row2 + 1); element.getStyle().setHeight(height, Unit.PX); } if (width == 0 || height == 0) { mergedCell.getElement().getStyle().setDisplay(Display.NONE); } else { mergedCell.getElement().getStyle().setProperty("display", "flex"); } } public void updateMergedRegionSize(MergedRegion region) { String key = toKey(region.col1, region.row1); Cell mergedCell = mergedCells.get(region.id); overflownMergedCells.remove(region); updateMergedRegionRegionSize(region, mergedCell); DivElement element = mergedCell.getElement(); // need to update the position of possible visible comment for merged // cell if (alwaysVisibleCellComments.containsKey(key)) { CellComment cellComment = alwaysVisibleCellComments.get(key); if (element.getStyle().getDisplay().equals(Display.NONE.getCssName())) { cellComment.hide(); } else { cellComment.refreshPositionAccordingToCellRightCorner(); } } } public void removeMergedRegion(MergedRegion region, int ruleIndex) { String key = toKey(region.col1, region.row1); jsniUtil.deleteRule(mergedRegionStyle, ruleIndex); MergedCell mCell = mergedCells.get(region.id); Cell originalCell = getCell(region.col1, region.row1); if (originalCell != null) { originalCell.setValue(mCell.getValue(), mCell.getCellStyle(), false); } mergedCells.remove(region.id).getElement().removeFromParent(); overflownMergedCells.remove(region); // paint new "released cells" as selected if (region.col1 >= selectionWidget.getCol1() && region.col2 <= selectionWidget.getCol2() && region.row1 >= selectionWidget.getRow1() && region.row2 <= selectionWidget.getRow2()) { updateSelectedCellStyles(region.col1, region.col2, region.row1, region.row2, false); } DivElement cellCommentReplacementElement = null; // move the possible cell comment to the correct cell if (cellHasComment(key)) { try { Cell cell = rows.get(region.row1 - firstRowIndex).get(region.col1 - firstColumnIndex); cell.showCellCommentMark(); cellCommentReplacementElement = cell.getElement(); } catch (Exception e) { // the cell just isn't visible, no problem. } } if (cellHasInvalidFormula(key)) { try { Cell cell = rows.get(region.row1 - firstRowIndex).get(region.col1 - firstColumnIndex); cell.showInvalidFormulaIndicator(); cellCommentReplacementElement = cell.getElement(); } catch (Exception e) { // the cell just isn't visible, no problem. } } if (alwaysVisibleCellComments.containsKey(key) && cellCommentReplacementElement != null) { CellComment cellComment = alwaysVisibleCellComments.get(key); cellComment.showDependingToCellRightCorner((Element) cellCommentReplacementElement.cast(), region.row1, region.col1); } if (customWidgetMap != null && customWidgetMap.containsKey(key)) { try { Cell cell = rows.get(region.row1 - firstRowIndex).get(region.col1 - firstColumnIndex); Widget customWidget = customWidgetMap.get(key); addCustomWidgetToCell(cell, customWidget); } catch (Exception e) { // the cell just isn't visible, no problem. } } } private boolean cellHasComment(String key) { return cellCommentsMap != null && cellCommentsMap.containsKey(key); } private boolean cellHasInvalidFormula(String key) { return invalidFormulaCells != null && invalidFormulaCells.contains(key); } public void setCellLinks(HashMap<String, String> cellLinksMap) { if (this.cellLinksMap == null) { this.cellLinksMap = cellLinksMap; } else { this.cellLinksMap.clear(); if (cellLinksMap != null) { this.cellLinksMap.putAll(cellLinksMap); } } if (cellLinksMap != null && !cellLinksMap.isEmpty()) { StringBuilder sb = new StringBuilder(); for (Iterator<String> i = cellLinksMap.keySet().iterator(); i.hasNext();) { String cssKey = i.next().replace("col", ".col").replace(" r", ".r"); sb.append(cssKey); if (i.hasNext()) { sb.append(","); } } if (hyperlinkStyle == null) { hyperlinkStyle = Document.get().createStyleElement(); hyperlinkStyle.setType("text/css"); hyperlinkStyle.setId(sheetId + "-hyperlinkstyle"); cellSizeAndPositionStyle.getParentElement().appendChild(hyperlinkStyle); sb.append(HYPERLINK_CELL_STYLE); jsniUtil.insertRule(hyperlinkStyle, sb.toString()); } else { jsniUtil.replaceSelector(hyperlinkStyle, sb.toString(), 0); } } else { if (hyperlinkStyle != null) { jsniUtil.replaceSelector(hyperlinkStyle, ".notusedselector", 0); } } } /** * NOTE: FOR INTERNAL USE ONLY, may be removed or changed in the future. * * @param key * key that identifies the cell position by column and row * @return {@code true} if cell belongs to a merged region, {@code false} * otherwise * @see #toKey(int, int) */ boolean isMergedCell(String key) { for (Cell cell : mergedCells.values()) { if (key.equals(toKey(cell.getCol(), cell.getRow()))) { return true; } } return false; } private Cell getMergedCell(String key) { for (Cell cell : mergedCells.values()) { if (key.equals(toKey(cell.getCol(), cell.getRow()))) { return cell; } } return null; } public void setInvalidFormulaCells(Set<String> newInvalidFormulaCells) { udpateInvalidFormulaCells(getAllCells(), newInvalidFormulaCells); updateMergedInvalidFormulaCells(newInvalidFormulaCells); invalidFormulaCells = (Set<String>) putNewValuesToCollectionField(newInvalidFormulaCells, invalidFormulaCells); updateAllVisibleComments(); } private void updateAllVisibleComments() { if (invalidFormulaCells == null) { return; } if (alwaysVisibleCellComments != null) { for (Entry<String, CellComment> entry : alwaysVisibleCellComments.entrySet()) { String key = entry.getKey(); CellComment cellComment = entry.getValue(); String errorMessage = invalidFormulaCells.contains(key) ? invalidFormulaMessage : null; cellComment.setInvalidFormulaMessage(errorMessage); } } if (cellCommentOverlay != null) { String errorMessage = invalidFormulaCells.contains(cellCommentCellClassName) ? invalidFormulaMessage : null; cellCommentOverlay.setInvalidFormulaMessage(errorMessage); } } private void udpateInvalidFormulaCells(Collection<Cell> allCells, Collection<String> newInvalidFormulaCells) { for (Cell cell : allCells) { String key = toKey(cell.getCol(), cell.getRow()); if (newInvalidFormulaCells != null && newInvalidFormulaCells.contains(key)) { cell.showInvalidFormulaIndicator(); } else if (invalidFormulaCells != null && invalidFormulaCells.contains(key)) { // remove cell.removeInvalidFormulaIndicator(); } } } public void setCellComments(HashMap<String, String> newCellCommentsMap, HashMap<String, String> newCellCommentAuthorsMap) { updateRowCellComments(getAllCells(), newCellCommentsMap); updateMergedCellCommentsMap(newCellCommentsMap); cellCommentsMap = putValuesToMapField(newCellCommentsMap, cellCommentsMap); cellCommentAuthorsMap = putValuesToMapField(newCellCommentAuthorsMap, cellCommentAuthorsMap); } private List<Cell> getAllCells() { ArrayList<Cell> cells = new ArrayList<Cell>(topLeftCells); for (List<Cell> row : topRightRows) { cells.addAll(row); } for (List<Cell> row : bottomLeftRows) { cells.addAll(row); } for (List<Cell> row : rows) { cells.addAll(row); } return cells; } private void updateRowCellComments(List<Cell> row, HashMap<String, String> newCellCommentsMap) { for (Cell cell : row) { String key = toKey(cell.getCol(), cell.getRow()); if (newCellCommentsMap != null && newCellCommentsMap.containsKey(key)) { cell.showCellCommentMark(); } else if (cellHasComment(key)) { // remove cell.removeCellCommentMark(); } } } private HashMap<String, String> putValuesToMapField(HashMap<String, String> newValuesMap, HashMap<String, String> cachedMap) { if (cachedMap != null) { cachedMap.clear(); if (newValuesMap != null) { cachedMap.putAll(newValuesMap); } } else { cachedMap = newValuesMap; } return cachedMap; } private <T> Collection<T> putNewValuesToCollectionField(Collection<T> newValues, Collection<T> cachedValues) { if (cachedValues != null) { cachedValues.clear(); if (newValues != null) { cachedValues.addAll(newValues); } } else { cachedValues = newValues; } return cachedValues; } private void updateMergedCellCommentsMap(HashMap<String, String> newCellCommentsMap) { for (Cell mc : mergedCells.values()) { String key = toKey(mc.getCol(), mc.getRow()); if (newCellCommentsMap != null && newCellCommentsMap.containsKey(key)) { mc.showCellCommentMark(); } else if (cellHasComment(key)) { // remove mc.removeCellCommentMark(); } } } private void updateMergedInvalidFormulaCells(Set<String> newInvalidFormulas) { for (Cell mc : mergedCells.values()) { String key = toKey(mc.getCol(), mc.getRow()); if (newInvalidFormulas != null && newInvalidFormulas.contains(key)) { mc.showInvalidFormulaIndicator(); } else if (cellHasInvalidFormula(key)) { // remove mc.removeInvalidFormulaIndicator(); } } } public void setCellCommentVisible(boolean visible, String key) { if (visible) { jsniUtil.parseColRow(key); final Cell cell; int parsedRow = jsniUtil.getParsedRow(); int parsedCol = jsniUtil.getParsedCol(); if (isMergedCell(key)) { cell = getMergedCell(key); } else { cell = getCell(parsedCol, parsedRow); } final CellComment cellComment = new CellComment(this, cell.getElement().getParentElement()); cellComment.setAuthor(cellCommentAuthorsMap.get(key)); cellComment.setCommentText(cellCommentsMap.get(key)); String errorMessage = invalidFormulaCells.contains(key) ? invalidFormulaMessage : null; cellComment.setInvalidFormulaMessage(errorMessage); cellComment.showDependingToCellRightCorner((Element) cell.getElement().cast(), parsedRow, parsedCol); alwaysVisibleCellComments.put(key, cellComment); } else { CellComment comment = alwaysVisibleCellComments.remove(key); if (comment != null) { // possible if sheet has been cleared comment.hide(); } } } public void refreshAlwaysVisibleCellCommentOverlays() { for (CellComment cellComment : alwaysVisibleCellComments.values()) { int row = cellComment.getRow(); int col = cellComment.getCol(); if (actionHandler.isColumnHidden(col) || actionHandler.isRowHidden(row) || !isCellRendered(col, row)) { cellComment.hide(); } else { cellComment.refreshPositionAccordingToCellRightCorner(); } } } public void refreshCurrentCellCommentOverlay() { if (cellCommentCellColumn != -1 && cellCommentCellRow != -1 && cellCommentCellClassName != null) { cellCommentOverlay.refreshPositionAccordingToCellRightCorner(); } } public void refreshPopupButtonOverlays() { if (sheetPopupButtons != null) { for (PopupButtonWidget pbw : sheetPopupButtons.values()) { if (pbw.isPopupOpen()) { pbw.openPopup(); } } } } protected void onCellCommentFocus(CellComment cellComment) { if (focusedCellCommentOverlay != null) { focusedCellCommentOverlay.pushBack(); } cellComment.bringForward(); focusedCellCommentOverlay = cellComment; } private void showCellComment(int column, int row) { String cellClassName = toKey(column, row); if (alwaysVisibleCellComments.containsKey(cellClassName)) { return; } final Element cellElement; if (isMergedCell(cellClassName)) { cellElement = getMergedCell(cellClassName).getElement().cast(); } else { cellElement = getCell(column, row).getElement(); cellCommentOverlay.setSheetElement(cellElement.getParentElement()); } cellCommentOverlay.setAuthor(cellCommentAuthorsMap.get(cellClassName)); cellCommentOverlay.setCommentText(cellCommentsMap.get(cellClassName)); String errorMessage = invalidFormulaCells.contains(cellClassName) ? invalidFormulaMessage : null; cellCommentOverlay.setInvalidFormulaMessage(errorMessage); cellCommentOverlay.show(cellElement, row, column); cellCommentCellClassName = cellClassName; } /** * Called when there is a MOUSEOVER or MOUSEOUT on a cell (cell element or * the triangle) that has a cell comment. * * @param event */ private void updateCellCommentDisplay(Event event, Element target) { int eventTypeInt = event.getTypeInt(); String targetClassName = target.getAttribute("class"); if (overlayShouldBeShownFor(targetClassName)) { Element cellElement = target.getParentElement().cast(); String cellElementClassName = cellElement.getAttribute("class"); if (cellElementClassName.endsWith(MERGED_CELL_CLASSNAME)) { cellElementClassName = cellElementClassName.replace(" " + MERGED_CELL_CLASSNAME, ""); } // if comment is always visible, skip it if (alwaysVisibleCellComments.containsKey(cellElementClassName)) { return; } if (eventTypeInt == Event.ONMOUSEOVER) { // MOUSEOVER triangle -> show comment unless already shown if (!(cellCommentOverlay.isVisible() && cellElementClassName.equals(cellCommentCellClassName))) { jsniUtil.parseColRow(cellElementClassName); cellCommentCellColumn = jsniUtil.getParsedCol(); cellCommentCellRow = jsniUtil.getParsedRow(); cellCommentHandler.trigger(); } } else { // MOUSEOUT triangle -> hide comment unless mouse moved on top // of the triangle's cell (parent) Element toElement = event.getRelatedEventTarget().cast(); if (!cellCommentEditMode && !toElement.equals(cellElement)) { cellCommentOverlay.hide(); cellCommentCellClassName = null; cellCommentCellColumn = -1; cellCommentCellRow = -1; } } } else { if (targetClassName.endsWith(MERGED_CELL_CLASSNAME)) { targetClassName = targetClassName.replace(" " + MERGED_CELL_CLASSNAME, ""); } // if comment is always visible, skip it if (alwaysVisibleCellComments.containsKey(targetClassName)) { return; } if (eventTypeInt == Event.ONMOUSEOVER) { // show comment unless already shown if (!(cellCommentOverlay.isVisible() && targetClassName.equals(cellCommentCellClassName))) { Event.setCapture(sheet); jsniUtil.parseColRow(targetClassName); cellCommentCellColumn = jsniUtil.getParsedCol(); cellCommentCellRow = jsniUtil.getParsedRow(); cellCommentHandler.trigger(); } } else if (eventTypeInt == Event.ONMOUSEOUT) { // MOUSEOUT triangle's cell -> hide unless mouse moved back on // top of the same triangle Element toElement = event.getRelatedEventTarget().cast(); if (!cellCommentEditMode && toElement != null && toElement.getParentElement() != null) { try { if (!(overlayShouldBeShownFor(toElement.getAttribute("class")) && toElement.getParentElement().equals(target))) { cellCommentOverlay.hide(); cellCommentCellClassName = null; cellCommentCellRow = -1; cellCommentCellColumn = -1; } } catch (NullPointerException npe) { debugConsole.warning( "SheetWidget:updateCellCommentDisplay: NPE ONMOUSEOUT, " + npe.getMessage()); } } } } } private void updateCellLinkTooltip(int eventTypeInt, int col, int row, String tooltip) { if (eventTypeInt == Event.ONMOUSEOVER) { hyperlinkTooltipLabel.setText(tooltip); final DivElement element; final String key = toKey(col, row); if (isMergedCell(key)) { element = getMergedCell(key).getElement(); } else { element = getCell(col, row).getElement(); } hyperlinkTooltip.setPopupPositionAndShow(new PositionCallback() { @Override public void setPosition(int offsetWidth, int offsetHeight) { setHyperlinkTooltipPosition(offsetWidth, offsetHeight, element); } }); } else { // mouseout hyperlinkTooltip.hide(); } } public void updateBottomRightCellValues(List<CellData> cellData2) { updateCellData(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex, rows, cellData2); } public void updateTopLeftCellValues(List<CellData> cellData2) { if (topLeftCells != null && !topLeftCells.isEmpty()) { Iterator<CellData> i = cellData2.iterator(); while (i.hasNext()) { CellData cd = i.next(); topLeftCells.get((cd.row - 1) * horizontalSplitPosition + cd.col - 1).setValue(cd.value, cd.cellStyle, cd.needsMeasure); String key = toKey(cd.col, cd.row); if (isMergedCell(key)) { getMergedCell(key).setValue(cd.value, cd.cellStyle, cd.needsMeasure); } if (cd.value == null) { cachedCellData.remove(key); } else { cachedCellData.put(key, cd); } } } // Update cell overflow state updateOverflows(false); } public void updateTopRightCellValues(List<CellData> cellData2) { updateCellData(1, verticalSplitPosition, firstColumnIndex, lastColumnIndex, topRightRows, cellData2); } public void updateBottomLeftCellValues(List<CellData> cellData2) { updateCellData(firstRowIndex, lastRowIndex, 1, horizontalSplitPosition, bottomLeftRows, cellData2); } private void updateCellData(int r1, int r2, int c1, int c2, ArrayList<ArrayList<Cell>> rows, List<CellData> cellData2) { if (rows == null || rows.isEmpty()) { return; } Iterator<CellData> i = cellData2.iterator(); ArrayList<Cell> row = null; int rowIndex = -1; while (i.hasNext()) { CellData cd = i.next(); if (cd.row >= r1 && cd.row <= r2 && cd.col >= c1 && cd.col <= c2) { if (rowIndex != cd.row) { row = rows.get(cd.row - r1); rowIndex = cd.row; } row.get(cd.col - c1).setValue(cd.value, cd.cellStyle, cd.needsMeasure); } String key = toKey(cd.col, cd.row); if (isMergedCell(key)) { getMergedCell(key).setValue(cd.value, cd.cellStyle, cd.needsMeasure); } if (cd.value == null) { cachedCellData.remove(key); } else { cachedCellData.put(key, cd); } } // Update cell overflow state updateOverflows(false); } public void cellValuesUpdated(ArrayList<CellData> updatedCellData) { // can contain cells from any of the panes -> just iterate and access for (CellData cd : updatedCellData) { String key = toKey(cd.col, cd.row); // update cache if (cd.value == null) { cachedCellData.remove(key); } else { cachedCellData.put(key, cd); } if (isMergedCell(key)) { getMergedCell(key).setValue(cd.value, cd.cellStyle, cd.needsMeasure); } else { Cell cell = null; if (isCellRenderedInScrollPane(cd.col, cd.row)) { cell = rows.get(cd.row - firstRowIndex).get(cd.col - firstColumnIndex); } else if (isCellRenderedInFrozenPane(cd.col, cd.row)) { cell = getFrozenCell(cd.col, cd.row); } if (cell != null) { cell.setValue(cd.value, cd.cellStyle, cd.needsMeasure); cell.markAsOverflowDirty(); } int j = verticalSplitPosition > 0 ? 0 : firstColumnIndex; for (; j < cd.col; j++) { Cell c = getCell(j, cd.row); if (c != null) { c.markAsOverflowDirty(); } } } } // Update cell overflow state updateOverflows(false); } /** * * @param row * 1-based row index * @return height in pixels */ private int getRowHeight(int row) { if (actionHandler.isRowHidden(row)) { return 0; } else if (row >= definedRowHeights.length) { return getDefaultRowHeight(); } else { return definedRowHeights[row - 1]; } } public int[] getRowHeights() { return definedRowHeights; } /** * * @return sheet default row height in pixels, converted from points */ private int getDefaultRowHeight() { if (defRowH == -1) { if (ppi == 0) { if (ppiCounter.hasParentElement()) { ppi = ppiCounter.getOffsetWidth(); } if (ppi == 0) { ppi = 96; } } defRowH = (int) (actionHandler.getDefaultRowHeight() * ppi / 72); } return defRowH; } /** * Converts the point value to pixels using the ppi for this client * * @param points * to convert * @return pixels */ private int convertPointsToPixel(float points) { return new BigDecimal(points * ppi / 72.0f).intValue(); } private float convertPixelsToPoint(int pixels) { return new BigDecimal(((float) pixels) / ppi * 72).floatValue(); } protected void handleInputElementValueChange(final boolean update) { if (!isSelectedCellCompletelyVisible()) { scrollSelectedCellIntoView(); } Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { String value = input.getValue(); recalculateInputElementWidth(value); if (update) { actionHandler.onCellInputValueChange(value); } } }); } private void recalculateInputElementWidth(final String value) { try { final Cell selectedCell = getSelectedCell(); selectedCell.setValue(value); int textWidth = measureValueWidth(selectedCell.getCellStyle(), value); int col = selectedCell.getCol(); int width; if (editingMergedCell) { MergedRegion region = actionHandler.getMergedRegionStartingFrom(selectedCellCol, selectedCellRow); col = region.col2; width = selectionWidget.countSum(actionHandler.getColWidths(), region.col1, region.col2 + 1); } else { width = actionHandler.getColWidthActual(col); } while (width < textWidth && col < actionHandler.getMaxColumns()) { width += actionHandler.getColWidthActual(++col); } input.setWidth((width + 1) + "px"); } catch (Exception e) { // cell is not visible yet, should not happen, but try again debugConsole.severe("SheetWidget:recalculateInputElementWidth: " + e.toString() + " while calculating input element width"); handleInputElementValueChange(false); } } /** * * @param col * 1 based * @param row * 1 based * @return */ public final static String toKey(int col, int row) { return "col" + col + " row" + row; } public final static String toCssKey(int col, int row) { return ".col" + col + ".row" + row; } /** * Clears the sheet. After this no headers or cells are visible. A * {@link #resetModel(SpreadsheetSettings)} call will make sheet visible * again. * * @param removed * if the widget is completely removed from DOM after this */ public void clearAll(boolean removed) { loaded = false; for (Widget i : getCustomWidgetIterator()) { remove(i); } customEditorWidget = null; for (SheetOverlay overlay : sheetOverlays.values()) { remove(overlay); } sheetOverlays.clear(); if (customWidgetMap != null) { customWidgetMap.clear(); customWidgetMap = null; } cleanDOM(); cachedCellData.clear(); scrollWidthCache.clear(); clearPositionStyles(); clearCellRangeStyles(); clearSelectedCellStyle(); clearBasicCellStyles(); clearMergedCells(); clearCellCommentsAndInvalidFormulas(); if (removed) { clearShiftedBorderCellStyles(); removeStyles(); if (previewHandlerRegistration != null) { previewHandlerRegistration.removeHandler(); previewHandlerRegistration = null; } } } public String getSelectedCellKey() { return toKey(selectedCellCol, selectedCellRow); } public int getSelectedCellColumn() { return selectedCellCol; } public int getSelectedCellRow() { return selectedCellRow; } public String getSelectedCellLatestValue() { CellData cd = cachedCellData.get(getSelectedCellKey()); return cd == null ? "" : cd.value; } public void setSelectedCell(int col, int row) { selectedCellRow = row; selectedCellCol = col; } public int getSelectionLeftCol() { return selectionWidget.getCol1(); } public int getSelectionRightCol() { return selectionWidget.getCol2(); } public int getSelectionTopRow() { return selectionWidget.getRow1(); } public int getSelectionBottomRow() { return selectionWidget.getRow2(); } public boolean isCoherentSelection() { return coherentSelection; } public void setCoherentSelection(boolean coherentSelection) { this.coherentSelection = coherentSelection; } public void setSelectionRangeOutlineVisible(boolean visible) { selectionWidget.setVisible(visible); } public boolean isSelectionRangeOutlineVisible() { return selectionWidget.isVisible(); } public void updateSelectionOutline(int col1, int col2, int row1, int row2) { if (isMergedCell(toKey(col2, row2))) { MergedRegion region = actionHandler.getMergedRegionStartingFrom(col2, row2); col2 = region.col2; row2 = region.row2; } selectionWidget.setPosition(col1, col2, row1, row2); } public void updateSelectedCellStyles(int col1, int col2, int row1, int row2, boolean replace) { cellRangeStylesCleared = false; // cells if (replace) { clearCellRangeStylesFromCells(); clearSelectedHeaderStyles(); Cell cell = getCell(selectedCellCol, selectedCellRow); highlightedCellCoord = null; if (cell != null) { cell.getElement().removeClassName(CELL_SELECTION_CLASSNAME); } } for (int r = row1; r <= row2; r++) { for (int c = col1; c <= col2; c++) { if (c != selectedCellCol || r != selectedCellRow) { Cell cell = getCell(c, r); cellRangeStyledCoords.add(new CellCoord(c, r)); if (cell != null) { cellRangeStyledCells.add(cell); cell.getElement().addClassName(CELL_RANGE_CLASSNAME); } Cell mergedCell = getMergedCell(toKey(c, r)); if (mergedCell != null) { cellRangeStyledCells.add(mergedCell); mergedCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } } } } // row headers for (int r = row1; r <= row2; r++) { selectRowHeader(r); } // column headers for (int c = col1; c <= col2; c++) { selectColHeader(c); } } private void selectColHeader(int c) { if (frozenColumnHeaders != null && frozenColumnHeaders.size() > c - 1) { selectedFrozenColHeaderIndexes.add(c); DivElement rh = frozenColumnHeaders.get(c - 1); rh.addClassName(SELECTED_COLUMN_HEADER_CLASSNAME); } else { selectedColHeaderIndexes.add(c); int targetCol = c - firstColumnIndex; if (targetCol >= 0 && colHeaders.size() > targetCol) { DivElement ch = colHeaders.get(targetCol); ch.addClassName(SELECTED_COLUMN_HEADER_CLASSNAME); } } } private void selectRowHeader(int r) { if (frozenRowHeaders != null && frozenRowHeaders.size() > r - 1) { selectedFrozenRowHeaderIndexes.add(r); DivElement rh = frozenRowHeaders.get(r - 1); rh.addClassName(SELECTED_ROW_HEADER_CLASSNAME); } else { selectedRowHeaderIndexes.add(r); int targetRow = r - firstRowIndex; if (targetRow >= 0 && rowHeaders.size() > targetRow) { DivElement rh = rowHeaders.get(targetRow); rh.addClassName(SELECTED_ROW_HEADER_CLASSNAME); } } } private void clearSelectedHeaderStyles() { for (DivElement rh : rowHeaders) { rh.removeClassName(SELECTED_ROW_HEADER_CLASSNAME); } for (DivElement ch : colHeaders) { ch.removeClassName(SELECTED_COLUMN_HEADER_CLASSNAME); } if (frozenRowHeaders != null) { for (DivElement rh : frozenRowHeaders) { rh.removeClassName(SELECTED_ROW_HEADER_CLASSNAME); } } if (frozenColumnHeaders != null) { for (DivElement ch : frozenColumnHeaders) { ch.removeClassName(SELECTED_COLUMN_HEADER_CLASSNAME); } } selectedRowHeaderIndexes.clear(); selectedColHeaderIndexes.clear(); selectedFrozenRowHeaderIndexes.clear(); selectedFrozenColHeaderIndexes.clear(); } private void clearCellRangeStylesFromCells() { for (Cell cell : cellRangeStyledCells) { cell.getElement().removeClassName(CELL_RANGE_CLASSNAME); } cellRangeStyledCells.clear(); cellRangeStyledCoords.clear(); } /** * Clears the light outline on the selected cell which is visible when the * selection is not coherent. */ public void clearSelectedCellStyle() { Cell cell = getCell(selectedCellCol, selectedCellRow); highlightedCellCoord = null; if (cell != null) { cell.getElement().removeClassName(CELL_SELECTION_CLASSNAME); } } /** * Clears the highlight (background) on selected cells and their * corresponding headers. */ public void clearCellRangeStyles() { clearSelectedHeaderStyles(); clearCellRangeStylesFromCells(); cellRangeStylesCleared = true; } protected void clearPositionStyles() { jsniUtil.clearCSSRules(cellSizeAndPositionStyle); } protected void clearBasicCellStyles() { jsniUtil.clearCSSRules(sheetStyle); // hyperlink style is created on-demand if (hyperlinkStyle != null) { jsniUtil.clearCSSRules(hyperlinkStyle); hyperlinkStyle.removeFromParent(); hyperlinkStyle = null; } } protected void clearShiftedBorderCellStyles() { jsniUtil.clearCSSRules(shiftedBorderCellStyle); } protected void clearMergedCells() { jsniUtil.clearCSSRules(mergedRegionStyle); for (Cell mergedCell : mergedCells.values()) { mergedCell.getElement().removeFromParent(); } mergedCells.clear(); } protected void clearCellCommentsAndInvalidFormulas() { cellCommentOverlay.hide(); for (CellComment cc : alwaysVisibleCellComments.values()) { cc.hide(); } alwaysVisibleCellComments.clear(); cellCommentsMap.clear(); cellCommentAuthorsMap.clear(); invalidFormulaCells.clear(); } /** * swaps the selected cell to the new one, which is the selected cell after * this call. * * the old cell is "highlighted" and the new one gets the selected cell * outline (when selection range outline is hidden). * * takes care of swapping into a merged cell (highlights correct all cell * headers). * * @param column * @param row */ public void swapCellSelection(int column, int row) { // highlight previously selected cell (background white->selected) // the headers for it are already highlighted // also remove the new selected cell from the highlighted cells (if it // is there). Cell oldSelectionCell = getCell(selectedCellCol, selectedCellRow); Cell oldMergedCell = getMergedCell(toKey(selectedCellCol, selectedCellRow)); if (cellRangeStylesCleared) { cellRangeStyledCoords.add(new CellCoord(selectedCellCol, selectedCellRow)); if (oldSelectionCell != null) { cellRangeStyledCells.add(oldSelectionCell); oldSelectionCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } if (oldMergedCell != null) { cellRangeStyledCells.add(oldMergedCell); oldMergedCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } cellRangeStylesCleared = false; } else { cellRangeStyledCoords.add(new CellCoord(selectedCellCol, selectedCellRow)); if (oldSelectionCell != null) { cellRangeStyledCells.add(oldSelectionCell); oldSelectionCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } if (oldMergedCell != null) { cellRangeStyledCells.add(oldMergedCell); oldMergedCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } // highlight the new selected cell headers MergedRegion region = actionHandler.getMergedRegionStartingFrom(column, row); selectRowHeader(row); if (region != null) { for (int i = region.row1 + 1; i <= region.row2; i++) { selectRowHeader(i); } } selectColHeader(column); if (region != null) { for (int i = region.col1 + 1; i <= region.col2; i++) { selectColHeader(i); } } } // mark the new selected cell with light outline if (oldSelectionCell != null) { highlightedCellCoord = null; oldSelectionCell.getElement().removeClassName(CELL_SELECTION_CLASSNAME); } if (oldMergedCell != null) { oldMergedCell.getElement().removeClassName(CELL_SELECTION_CLASSNAME); } Cell newSelectionCell = getCell(column, row); if (newSelectionCell != null) { highlightedCellCoord = new CellCoord(newSelectionCell.getCol(), newSelectionCell.getRow()); newSelectionCell.getElement().addClassName(CELL_SELECTION_CLASSNAME); } Cell newMergedSelectionCell = getMergedCell(toKey(column, row)); if (newMergedSelectionCell != null) { newMergedSelectionCell.getElement().addClassName(CELL_SELECTION_CLASSNAME); } setSelectedCell(column, row); } /** * Swaps the selected cell to the new one, which is the selected cell after * this call. * * The old cell is "highlighted" and the new selected cell will be marked as * selected. This method is different to * {@link #swapCellSelection(int, int)} because the selected cell should be * inside the old selection, instead of adding a new cell into the * selection. No need to modify highlighted headers. * * @param col * @param row */ public void swapSelectedCellInsideSelection(int col, int row) { Cell newSelectionCell = getCell(col, row); Cell newMergedSelectionCell = getMergedCell(toKey(col, row)); Cell oldSelectionCell = getCell(selectedCellCol, selectedCellRow); Cell oldMergedSelectionCell = getMergedCell(toKey(selectedCellCol, selectedCellRow)); cellRangeStyledCoords.add(new CellCoord(selectedCellCol, selectedCellRow)); if (oldSelectionCell != null) { cellRangeStyledCells.add(oldSelectionCell); oldSelectionCell.getElement().removeClassName(CELL_SELECTION_CLASSNAME); oldSelectionCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } if (oldMergedSelectionCell != null) { cellRangeStyledCells.add(oldMergedSelectionCell); oldMergedSelectionCell.getElement().removeClassName(CELL_SELECTION_CLASSNAME); oldMergedSelectionCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } cellRangeStyledCoords.remove(new CellCoord(col, row)); if (newSelectionCell != null) { cellRangeStyledCells.remove(newSelectionCell); newSelectionCell.getElement().removeClassName(CELL_RANGE_CLASSNAME); } if (newMergedSelectionCell != null) { cellRangeStyledCells.remove(newMergedSelectionCell); newMergedSelectionCell.getElement().removeClassName(CELL_RANGE_CLASSNAME); } setSelectedCell(col, row); } /** * Marks the given interval as selected (highlighted background), replaces * old selected cells. Ignores the currently selected cell. * * @param col1 * @param col2 * @param row1 * @param row2 */ public void replaceAsSelectedCells(int col1, int col2, int row1, int row2) { clearCellRangeStylesFromCells(); for (int r = row1; r <= row2; r++) { for (int c = col1; c <= col2; c++) { if (selectedCellCol != c || selectedCellRow != r) { Cell cell = getCell(c, r); cellRangeStyledCoords.add(new CellCoord(c, r)); if (cell != null) { cellRangeStyledCells.add(cell); cell.getElement().addClassName(CELL_RANGE_CLASSNAME); } Cell mergedCell = getMergedCell(toKey(c, r)); if (mergedCell != null) { cellRangeStyledCells.add(mergedCell); mergedCell.getElement().addClassName(CELL_RANGE_CLASSNAME); } } } } } /** * Replaces the currently marked selected headers (highlighted) with the * given intervals. * * @param row1 * @param row2 * @param col1 * @param col2 */ public void replaceHeadersAsSelected(int row1, int row2, int col1, int col2) { clearSelectedHeaderStyles(); // row headers for (int r = row1; r <= row2; r++) { selectRowHeader(r); } // column headers for (int c = col1; c <= col2; c++) { selectColHeader(c); } } public int[] getSheetDisplayRange() { return new int[] { firstRowIndex, firstColumnIndex, lastRowIndex, lastColumnIndex }; } public boolean hasFrozenColumns() { return horizontalSplitPosition > 0; } public boolean hasFrozenRows() { return verticalSplitPosition > 0; } /** * * @return the first column index that is completely visible on the left */ public int getLeftVisibleColumnIndex() { int index = firstColumnIndex; final int bound = sheet.getAbsoluteLeft(); ArrayList<Cell> firstVisibleRow = new ArrayList<Cell>(); int firstVisibleRowIndex = 0; for (firstVisibleRowIndex = 0; firstVisibleRowIndex < rows.size(); firstVisibleRowIndex++) { if (!actionHandler.isRowHidden(firstVisibleRowIndex + 1)) { firstVisibleRow = rows.get(firstVisibleRowIndex); } } for (Cell cell : firstVisibleRow) { if (cell.getElement().getAbsoluteLeft() >= bound) { return index; } else { index++; } } return firstColumnIndex; } public int getRightVisibleColumnIndex() { int index = lastColumnIndex; final List<Cell> cells = rows.get(0); int size = cells.size(); for (int i = size - 1; i > 0; i--) { if (isVisible(cells.get(i).getElement())) { return index; } else { index--; } } return lastColumnIndex; } private Cell getFirstVisibleCellInRow(ArrayList<Cell> row) { // column indexing starts from 1 int columnIndex = 0; for (Cell cell : row) { if (!actionHandler.isColumnHidden(columnIndex + 1)) { return row.get(columnIndex); } columnIndex++; } return null; } /** * * @return the first row index that is completely visible on the top */ public int getTopVisibleRowIndex() { int index = firstRowIndex; final int bound = sheet.getAbsoluteTop(); for (ArrayList<Cell> row : rows) { Cell cell = getFirstVisibleCellInRow(row); if (cell != null && cell.getElement().getAbsoluteTop() >= bound) { return index; } else { index++; } } return firstRowIndex; } public int getBottomVisibleRowIndex() { int index = lastRowIndex; final int bound = sheet.getAbsoluteBottom(); for (int i = rows.size() - 1; i > 0; i--) { if (rows.get(i).get(0).getElement().getAbsoluteBottom() <= bound) { return index; } else { index--; } } return lastRowIndex; } public void displayCustomCellEditor(Widget customEditorWidget) { customCellEditorDisplayed = true; jsniUtil.replaceSelector(editedCellFreezeColumnStyle, ".notusedselector", 0); this.customEditorWidget = customEditorWidget; Cell selectedCell = getSelectedCell(); selectedCell.setValue(null); Widget parent = customEditorWidget.getParent(); if (parent != null && !equals(parent)) { customEditorWidget.removeFromParent(); } DivElement element = selectedCell.getElement(); element.addClassName(CUSTOM_EDITOR_CELL_CLASSNAME); element.appendChild(customEditorWidget.getElement()); if (parent == null || (parent != null && !equals(parent))) { adopt(customEditorWidget); } focusSheet(); } public void removeCustomCellEditor() { if (customCellEditorDisplayed) { customCellEditorDisplayed = false; customEditorWidget.getElement().removeClassName(CUSTOM_EDITOR_CELL_CLASSNAME); orphan(customEditorWidget); customEditorWidget.removeFromParent(); // the cell value should have been updated if (loaded && getSelectedCell() != null) { CellData cd = cachedCellData.get(getSelectedCellKey()); if (cd == null) { getSelectedCell().setValue(null); } else { getSelectedCell().setValue(cd.value); } } customEditorWidget = null; } } private Cell getSelectedCell() { String selectedCellKey = getSelectedCellKey(); if (isMergedCell(selectedCellKey)) { return getMergedCell(selectedCellKey); } else { return getCell(selectedCellCol, selectedCellRow); } } boolean isCellRenderedInFrozenPane(int col, int row) { return (row <= verticalSplitPosition && (col >= firstColumnIndex && col <= lastColumnIndex || col <= horizontalSplitPosition)) || (col <= horizontalSplitPosition && (row >= firstRowIndex && row <= lastRowIndex || row <= verticalSplitPosition)); } private Cell getFrozenCell(int col, int row) { int colArrayIndex = col - 1; int rowArrayIndex = row - 1; if (rowArrayIndex < 0 || colArrayIndex < 0) { return null; } if (verticalSplitPosition < row) { // Cell is in bottom left pane boolean rowIndexValid = row >= firstRowIndex; boolean rowAvailable = bottomLeftRows.size() > row - firstRowIndex; if (rowIndexValid && rowAvailable) { boolean colAvailable = bottomLeftRows.get(row - firstRowIndex).size() > colArrayIndex; if (colAvailable) { return bottomLeftRows.get(row - firstRowIndex).get(colArrayIndex); } } } else if (horizontalSplitPosition < col) { // Cell is in top right pane int colIndexInPane = col - firstColumnIndex; boolean rowAvailable = topRightRows.size() > rowArrayIndex; if (rowAvailable) { boolean colIndexValid = col >= firstColumnIndex; boolean colAvailable = topRightRows.get(rowArrayIndex).size() > colIndexInPane; if (colIndexValid && colAvailable) { return topRightRows.get(rowArrayIndex).get(colIndexInPane); } } } else { // Cell is in top left pane int cellIndex = rowArrayIndex * horizontalSplitPosition + colArrayIndex; boolean cellAvailable = topLeftCells.size() > cellIndex; if (cellIndex >= 0 && cellAvailable) { return topLeftCells.get(cellIndex); } } return null; } /** * Returns the cell. Checks for it from a freeze pane. * * @param col * @param row * @return */ Cell getCell(int col, int row) { if (isCellRenderedInFrozenPane(col, row)) { return getFrozenCell(col, row); } else { int fixedColIndex = col - firstColumnIndex; int fixedRowIndex = row - firstRowIndex; if (fixedColIndex < 0 || fixedRowIndex < 0) { return null; } boolean rowAvailable = rows.size() > fixedRowIndex; if (rowAvailable) { boolean colAvailable = rows.get(fixedRowIndex).size() > fixedColIndex; if (colAvailable) { return rows.get(fixedRowIndex).get(fixedColIndex); } } } return null; } private String getSelectedCellCellStyleString() { CellData cellData = getCellData(selectedCellCol, selectedCellRow); return cellData == null ? "cs0" : cellData.cellStyle; } public void startEditingCell(boolean focus, boolean recalculate, final String value) { editingCell = true; jsniUtil.replaceSelector(editedCellFreezeColumnStyle, "." + sheetId + " .sheet div" + toCssKey(selectedCellCol, selectedCellRow), 0); input.setStyleName( toKey(selectedCellCol, selectedCellRow) + " cell" + " " + getSelectedCellCellStyleString()); if (isMergedCell(toKey(selectedCellCol, selectedCellRow))) { editingMergedCell = true; input.setHeight( getMergedCell(toKey(selectedCellCol, selectedCellRow)).getElement().getStyle().getHeight()); } updateInputParent(); if (recalculate) { handleInputElementValueChange(false); } if (focus) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { // @Override public void execute() { input.setFocus(true); if (value.endsWith("%")) { input.setCursorPos(value.length() - 1); } else { // continue editing at end pos input.setCursorPos(value.length()); } } }); } input.setValue(value); } private void updateInputParent() { Element parent = DOM.getParent(input.getElement()); Element newParent; if (selectedCellRow <= verticalSplitPosition) { if (selectedCellCol <= horizontalSplitPosition) { newParent = topLeftPane; } else { newParent = topRightPane; } } else if (selectedCellCol <= horizontalSplitPosition) { newParent = bottomLeftPane; } else { newParent = sheet; } if (parent != newParent) { parent.removeChild(input.getElement()); DOM.appendChild(newParent, input.getElement()); } } public void updateSelectedCellValue(String value) { if (isSelectedCellRendered()) { getSelectedCell().setValue(value); } int j = verticalSplitPosition > 0 ? 0 : firstColumnIndex; for (; j < getSelectedCellColumn(); j++) { Cell cell = getCell(j, getSelectedCellRow()); if (cell != null) { cell.markAsOverflowDirty(); } } // Update cell overflow state updateOverflows(false); } private void updateOverflows(boolean forced) { if (forced) { markRowsAsDirty(rows); markRowsAsDirty(topRightRows); markRowsAsDirty(bottomLeftRows); for (Cell cell : topLeftCells) { if (cell != null) { cell.markAsOverflowDirty(); } } } overflowUpdater.schedule(SCROLL_HANDLER_TRIGGER_DELAY); } private void markRowsAsDirty(ArrayList<ArrayList<Cell>> rows) { if (rows != null) { for (ArrayList<Cell> row : rows) { for (Cell cell : row) { if (cell != null) { cell.markAsOverflowDirty(); } } } } } private Timer overflowUpdater = new Timer() { private void measureCell(int col, int row) { Cell cell = getCell(col, row); if (cell != null && cell.isOverflowDirty()) { cell.measureOverflow(); } } private void measureCells(int fromRow, int toRow, int fromCol, int toCol) { for (int i = fromRow; i <= toRow; i++) { for (int j = fromCol; j <= toCol; j++) { measureCell(j, i); } } } private void updateCell(int col, int row) { Cell cell = getCell(col, row); if (cell != null && cell.isOverflowDirty()) { cell.updateOverflow(); } } private void updateCells(int fromRow, int toRow, int fromCol, int toCol) { for (int i = fromRow; i <= toRow; i++) { for (int j = fromCol; j <= toCol; j++) { updateCell(j, i); } } } @Override public void run() { // First measure all // Bottom right pane measureCells(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex); // Top left pane measureCells(0, verticalSplitPosition, 0, horizontalSplitPosition); // Top right pane measureCells(0, verticalSplitPosition, firstColumnIndex, lastColumnIndex); // Bottom left pane measureCells(firstRowIndex, lastRowIndex, 0, horizontalSplitPosition); // Then update contents // Bottom right pane updateCells(firstRowIndex, lastRowIndex, firstColumnIndex, lastColumnIndex); // Top left pane updateCells(0, verticalSplitPosition, 0, horizontalSplitPosition); // Top right pane updateCells(0, verticalSplitPosition, firstColumnIndex, lastColumnIndex); // Bottom left pane updateCells(firstRowIndex, lastRowIndex, 0, horizontalSplitPosition); } }; public void updateInputValue(String value) { if (customCellEditorDisplayed) { // Do nothing here because the value ought to come from server side } else { input.setValue(value); if (editingCell) { handleInputElementValueChange(false); } } } public void stopEditingCell(boolean focusSheet) { editingCell = false; editingMergedCell = false; jsniUtil.replaceSelector(editedCellFreezeColumnStyle, ".notusedselector", 0); input.setValue(""); input.setWidth("0"); input.setHeight(""); input.setStyleName(""); if (focusSheet) { focusSheet(); } } public void focusSheet() { focusSheet(true); } public void focusSheet(boolean doAsDeferred) { if (doAsDeferred) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { sheet.focus(); } }); } else { sheet.focus(); } } public boolean isSelectedCellRendered() { return isCellRenderedInScrollPane(selectedCellCol, selectedCellRow) || isCellRenderedInFrozenPane(selectedCellCol, selectedCellRow); } public boolean isSelectedCellCompletelyVisible() { return isCellCompletelyVisible(selectedCellCol, selectedCellRow); } public boolean isSelectionAreaCompletelyVisible() { return isAreaCompletelyVisible(selectionWidget.getCol1(), selectionWidget.getCol2(), selectionWidget.getRow1(), selectionWidget.getRow2()); } public boolean isCellRenderedInScrollPane(int col, int row) { return col >= firstColumnIndex && col <= lastColumnIndex && row >= firstRowIndex && row <= lastRowIndex; } /** * Is the cell currently rendered in any of the frozen panes. * * @param col * @param row * @return */ public boolean isFrozenCellRendered(int col, int row) { return isCellRenderedInTopLeftPane(col, row) || isCellRenderedInTopRightPane(col, row) || isCellRenderedInBottomLeftPane(col, row); } /** * Is the cell currently rendered in top left pane. * * @param col * @param row * @return */ public boolean isCellRenderedInTopLeftPane(int col, int row) { return col <= horizontalSplitPosition && row <= verticalSplitPosition; } /** * Is the cell currently rendered in top right pane. * * @param col * @param row * @return */ public boolean isCellRenderedInTopRightPane(int col, int row) { return col > horizontalSplitPosition && col <= lastColumnIndex && row <= verticalSplitPosition; } /** * Is the cell currently rendered in bottom left pane. * * @param col * @param row * @return */ public boolean isCellRenderedInBottomLeftPane(int col, int row) { return row > verticalSplitPosition && row <= lastRowIndex && col <= horizontalSplitPosition; } /** * Is the given cell currently rendered. Checks freeze panes too. * * @param col * @param row * @return */ public boolean isCellRendered(int col, int row) { return isCellRenderedInScrollPane(col, row) || isFrozenCellRendered(col, row); } /** * Is the given cell currently visible completely. Checks freeze panes too. * * @param col * @param row * @return */ public boolean isCellCompletelyVisible(int col, int row) { return (col <= horizontalSplitPosition || col >= getLeftVisibleColumnIndex() && col <= getRightVisibleColumnIndex()) && (row <= verticalSplitPosition || row <= getTopVisibleRowIndex() && row >= getBottomVisibleRowIndex()); } public boolean isAreaCompletelyVisible(int col1, int col2, int row1, int row2) { return isCellCompletelyVisible(col1, row1) && isCellRendered(col1, row2) && isCellRendered(col2, row1) && isCellRendered(col2, row2); } public void scrollSelectedCellIntoView() { scrollCellIntoView(selectedCellCol, selectedCellRow); } /** * Scrolls the sheet to show the given cell, then triggers escalator for * updating cells if necessary. * * This method does the {@link #isCellRenderedInScrollPane(int, int)} in * itself, so no need to do the check before calling this. Nothing is done * if the cell is already visible. * * Scrolls one cell extra to all directions to cut the scrolls to half when * using keyboard navigation. * * @param col * 1-based * @param row * 1-based */ public void scrollCellIntoView(int col, int row) { boolean scrolled = false; // vertical: final int leftColumnIndex = getLeftVisibleColumnIndex(); if (col < leftColumnIndex && col > horizontalSplitPosition) { // scroll to left until column is visible (+ 1 cell extra) int scroll = 0; for (int i = leftColumnIndex - 1; i >= col - 1 && i > 0; i--) { scroll += actionHandler.getColWidthActual(i); } sheet.setScrollLeft(sheet.getScrollLeft() - scroll); if (col <= firstColumnIndex || scroll > (actionHandler.getColumnBufferSize() / 2)) { scrolled = true; } } else { final int rightColumnIndex = getRightVisibleColumnIndex(); if (col > rightColumnIndex) { // scroll to right until column is visible (+ 1 cell extra) int scroll = 0; final int maximumCols = actionHandler.getMaxColumns(); for (int i = rightColumnIndex + 1; i <= col + 1 && i <= maximumCols; i++) { scroll += actionHandler.getColWidthActual(i); } sheet.setScrollLeft(sheet.getScrollLeft() + scroll); if (col >= lastColumnIndex || scroll > (actionHandler.getColumnBufferSize() / 2)) { scrolled = true; } } } // horizontal: final int topRowIndex = getTopVisibleRowIndex(); if (row < topRowIndex && row > verticalSplitPosition) { // scroll up until row is visible (+ 1 cell extra) int scroll = 0; for (int i = topRowIndex - 1; i >= row - 1 && i > 0; i--) { scroll += getRowHeight(i); } // with horizontal need to add 1 pixel per cell because borders scroll += (topRowIndex - row); final int result = sheet.getScrollTop() - scroll; sheet.setScrollTop(result > 0 ? result : 0); if (row <= firstRowIndex || scroll > (actionHandler.getRowBufferSize() / 2)) { scrolled = true; } } else { final int bottomRowIndex = getBottomVisibleRowIndex(); if (row > bottomRowIndex) { // scroll down until row is visible (+1 cell extra) int scroll = 0; final int maximumRows = actionHandler.getMaxRows(); for (int i = bottomRowIndex + 1; i <= row + 1 && i <= maximumRows; i++) { scroll += getRowHeight(i); } // with horizontal need to add 1 pixel per cell because borders scroll += (row - bottomRowIndex); sheet.setScrollTop(sheet.getScrollTop() + scroll); if (row >= lastRowIndex || scroll > (actionHandler.getRowBufferSize() / 2)) { scrolled = true; } } } if (scrolled) { onSheetScroll(); moveHeadersToMatchScroll(); } } public void scrollSelectionAreaIntoView() { scrollAreaIntoView(selectionWidget.getCol1(), selectionWidget.getCol2(), selectionWidget.getRow1(), selectionWidget.getRow2()); } boolean scrollAreaIntoViewHorizontally(int col1, int col2, boolean actOnLeftEdge) { boolean scrolled = false; // horizontal: if (col1 <= horizontalSplitPosition) { col1 = horizontalSplitPosition + 1; } final int leftColumnIndex = getLeftVisibleColumnIndex(); final int rightColumnIndex = getRightVisibleColumnIndex(); if (actOnLeftEdge) { if (col1 < leftColumnIndex) { // scroll to left until col1 comes visible int scroll = 0; for (int i = leftColumnIndex - 1; i >= col1 - 1 && i > 0; i--) { scroll += actionHandler.getColWidthActual(i); } sheet.setScrollLeft(sheet.getScrollLeft() - scroll); if (col1 <= firstColumnIndex || scroll > (actionHandler.getColumnBufferSize() / 2)) { scrolled = true; } } else if (col1 > rightColumnIndex) { // scroll to right until col1 comes visible int scroll = 0; final int maximumCols = actionHandler.getMaxColumns(); for (int i = rightColumnIndex + 1; i <= col1 + 1 && i <= maximumCols; i++) { scroll += actionHandler.getColWidthActual(i); } sheet.setScrollLeft(sheet.getScrollLeft() + scroll); if (col1 >= lastColumnIndex || scroll > (actionHandler.getColumnBufferSize() / 2)) { scrolled = true; } } } else { if (col2 > rightColumnIndex) { // scroll right until col2 comes visible int scroll = 0; final int maximumCols = actionHandler.getMaxColumns(); for (int i = rightColumnIndex + 1; i <= col2 + 1 && i <= maximumCols; i++) { scroll += actionHandler.getColWidthActual(i); } sheet.setScrollLeft(sheet.getScrollLeft() + scroll); if (col2 >= lastColumnIndex || scroll > (actionHandler.getColumnBufferSize() / 2)) { scrolled = true; } } else if (col2 < leftColumnIndex) { // scroll to left until col2 comes visible int scroll = 0; for (int i = leftColumnIndex - 1; i >= col2 - 1 && i > 0; i--) { scroll += actionHandler.getColWidthActual(i); } sheet.setScrollLeft(sheet.getScrollLeft() - scroll); if (col2 <= firstColumnIndex || scroll > (actionHandler.getColumnBufferSize() / 2)) { scrolled = true; } } } return scrolled; } boolean scrollAreaIntoViewVertically(int row1, int row2, boolean actOnTopEdge) { boolean scrolled = false; // vertical: if (row1 <= verticalSplitPosition) { row1 = verticalSplitPosition + 1; } final int topRowIndex = getTopVisibleRowIndex(); final int bottomRowIndex = getBottomVisibleRowIndex(); if (actOnTopEdge) { if (row1 < topRowIndex) { // scroll up until the row1 come visible int scroll = 0; for (int i = topRowIndex - 1; i >= row1 - 1 && i > 0; i--) { scroll += getRowHeight(i); } // with horizontal need to add 1 pixel per cell because borders scroll += (topRowIndex - row1); final int result = sheet.getScrollTop() - scroll; sheet.setScrollTop(result > 0 ? result : 0); if (row1 <= firstRowIndex || scroll > (actionHandler.getRowBufferSize() / 2)) { scrolled = true; } } else if (row1 > bottomRowIndex) { // scroll down until row1 is visible int scroll = 0; final int maximumRows = actionHandler.getMaxRows(); for (int i = bottomRowIndex + 1; i <= row1 + 1 && i <= maximumRows; i++) { scroll += getRowHeight(i); } // with horizontal need to add 1 pixel per cell because borders scroll += (row1 - bottomRowIndex); sheet.setScrollTop(sheet.getScrollTop() + scroll); if (row1 >= lastRowIndex || scroll > (actionHandler.getRowBufferSize() / 2)) { scrolled = true; } } } else { if (row2 > bottomRowIndex) { // scroll down until row2 is visible int scroll = 0; final int maximumRows = actionHandler.getMaxRows(); for (int i = bottomRowIndex + 1; i <= row2 + 1 && i <= maximumRows; i++) { scroll += getRowHeight(i); } // with horizontal need to add 1 pixel per cell because borders scroll += (row2 - bottomRowIndex); sheet.setScrollTop(sheet.getScrollTop() + scroll); if (row2 >= lastRowIndex || scroll > (actionHandler.getRowBufferSize() / 2)) { scrolled = true; } } else if (row2 < topRowIndex) { // scroll up until the row2 come visible int scroll = 0; for (int i = topRowIndex - 1; i >= row2 - 1 && i > 0; i--) { scroll += getRowHeight(i); } // with horizontal need to add 1 pixel per cell because borders scroll += (topRowIndex - row2); final int result = sheet.getScrollTop() - scroll; sheet.setScrollTop(result > 0 ? result : 0); if (row2 <= firstRowIndex || scroll > (actionHandler.getRowBufferSize() / 2)) { scrolled = true; } } } return scrolled; } /** * Scrolls the sheet to show the given area, or as much of it as fits into * the view. * * @param col1 * 1-based * @param col2 * 1-based * @param row1 * 1-based * @param row2 * 1-based */ public void scrollAreaIntoView(int col1, int col2, int row1, int row2) { boolean scrolled = scrollAreaIntoViewHorizontally(col1, col2, true); if (scrollAreaIntoViewVertically(row1, row2, true)) { scrolled = true; } if (scrolled) { onSheetScroll(); moveHeadersToMatchScroll(); } } public void addShiftedCellBorderStyles(List<String> styles) { if (styles.size() > 0) { StringBuilder sb = new StringBuilder(getRules(shiftedBorderCellStyle)); for (String style : styles) { try { sb.append(style.replace(".col", ".v-spreadsheet." + sheetId + " .cell.col")); } catch (Exception e) { debugConsole.log(Level.SEVERE, "Invalid custom cell border style: " + style + ", " + e.getMessage()); } } shiftedBorderCellStyle.removeAllChildren(); shiftedBorderCellStyle.appendChild(Document.get().createTextNode(sb.toString())); } } public void removeShiftedCellBorderStyles() { jsniUtil.clearCSSRules(shiftedBorderCellStyle); } public int getSheetScrollLeft() { return sheet.getScrollLeft(); } public int getSheetScrollTop() { return sheet.getScrollTop(); } public void setScrollPosition(int scrollLeft, int scrollTop) { sheet.setScrollLeft(scrollLeft); sheet.setScrollTop(scrollTop); } public void addPopupButton(PopupButtonWidget popupButton) { if (sheetPopupButtons == null) { sheetPopupButtons = new HashMap<String, PopupButtonWidget>(); } int col = popupButton.getCol(); int row = popupButton.getRow(); String key = toKey(col, row); if (col != 0 && row != 0) { // on first load col and row might be 0 sheetPopupButtons.put(key, popupButton); if (isCellRendered(col, row)) { Cell cell = getCell(col, row); Widget parent = popupButton.getParent(); if (parent != null) { if (equals(parent)) { cell.showPopupButton(popupButton.getElement()); } else { popupButton.removeFromParent(); cell.showPopupButton(popupButton.getElement()); adopt(popupButton); } } else { cell.showPopupButton(popupButton.getElement()); adopt(popupButton); } } } else { // if getting 0 col / row, still need to store the popupbutton while (sheetPopupButtons.containsKey(key)) { popupButton.setCol(--col); key = toKey(col, row); } sheetPopupButtons.put(key, popupButton); } popupButton.setSheetWidget(this, sheet); } public void removePopupButton(PopupButtonWidget popupButton) { int col = popupButton.getCol(); int row = popupButton.getRow(); sheetPopupButtons.remove(toKey(col, row)); remove(popupButton); if (col >= firstColumnIndex && col <= lastColumnIndex && row >= firstRowIndex && row <= lastRowIndex) { // need to remove the possible reference from the cell too getCell(col, row).removePopupButton(); } } /** * PopupButtons should not change position (cell), but the popupButton's * col&row values come after the popupButton has actually been added. */ public void updatePopupButtonPosition(PopupButtonWidget popupButton, int oldRow, int oldCol, int newRow, int newCol) { sheetPopupButtons.remove(toKey(oldCol, oldRow)); sheetPopupButtons.put(toKey(newCol, newRow), popupButton); Widget parent = popupButton.getParent(); // convert to if (isCellRendered(newCol, newRow)) { Cell cell = getCell(newCol, newRow); if (parent != null) { if (equals(parent)) { if (isCellRendered(oldCol, oldRow)) { getCell(oldCol, oldRow).removePopupButton(); } cell.showPopupButton(popupButton.getElement()); } else { popupButton.removeFromParent(); cell.showPopupButton(popupButton.getElement()); adopt(popupButton); } } else { cell.showPopupButton(popupButton.getElement()); adopt(popupButton); } } else if (parent != null) { popupButton.removeFromParent(); } } // This is for GWT @Override public Iterator<Widget> iterator() { final List<Widget> resultList = new ArrayList<Widget>(); resultList.add(input); resultList.addAll(getCustomWidgetIterator()); return resultList.iterator(); } // This is for clearing of sheet from custom widgets protected Collection<Widget> getCustomWidgetIterator() { final List<Widget> emptyList = new ArrayList<Widget>(); if (customEditorWidget != null) { emptyList.add(customEditorWidget); } emptyList.addAll(sheetOverlays.values()); if (customWidgetMap != null) { emptyList.addAll(customWidgetMap.values()); } if (sheetPopupButtons != null) { emptyList.addAll(sheetPopupButtons.values()); } return emptyList; } @Override public boolean remove(Widget child) { try { Element element = child.getElement(); com.google.gwt.dom.client.Element parentElement = element.getParentElement(); Widget widgetParent = child.getParent(); boolean isAttachedToPanes = sheet.equals(parentElement) || topLeftPane.equals(parentElement) || topRightPane.equals(parentElement) || bottomLeftPane.equals(parentElement); if (isAttachedToPanes || child.equals(customEditorWidget) || (parentElement != null && parentElement.getParentNode() != null && sheet.isOrHasChild(parentElement.getParentNode()))) { orphan(child); element.removeFromParent(); return true; } else if (equals(widgetParent)) { orphan(child); return true; } else { return false; } } catch (Exception e) { debugConsole.log(Level.WARNING, "Exception while removing child widget from SheetWidget"); } return false; } private void setHyperlinkTooltipPosition(int offsetWidth, int offsetHeight, DivElement element) { // Calculate left position for the popup. The computation for // the left position is bidi-sensitive. int textBoxOffsetWidth = element.getOffsetWidth(); // Compute the difference between the popup's width and the // textbox's width int offsetWidthDiff = offsetWidth - textBoxOffsetWidth; int left; if (LocaleInfo.getCurrentLocale().isRTL()) { // RTL case int textBoxAbsoluteLeft = element.getAbsoluteLeft(); // Right-align the popup. Note that this computation is // valid in the case where offsetWidthDiff is negative. left = textBoxAbsoluteLeft - offsetWidthDiff; // If the suggestion popup is not as wide as the text box, always // align to the right edge of the text box. Otherwise, figure out // whether // to right-align or left-align the popup. if (offsetWidthDiff > 0) { // Make sure scrolling is taken into account, since // box.getAbsoluteLeft() takes scrolling into account. int windowRight = Window.getClientWidth() + Window.getScrollLeft(); int windowLeft = Window.getScrollLeft(); // Compute the left value for the right edge of the textbox int textBoxLeftValForRightEdge = textBoxAbsoluteLeft + textBoxOffsetWidth; // Distance from the right edge of the text box to the right // edge // of the window int distanceToWindowRight = windowRight - textBoxLeftValForRightEdge; // Distance from the right edge of the text box to the left edge // of the // window int distanceFromWindowLeft = textBoxLeftValForRightEdge - windowLeft; // If there is not enough space for the overflow of the popup's // width to the right of the text box and there IS enough space // for the // overflow to the right of the text box, then left-align the // popup. // However, if there is not enough space on either side, stick // with // right-alignment. if (distanceFromWindowLeft < offsetWidth && distanceToWindowRight >= offsetWidthDiff) { // Align with the left edge of the text box. left = textBoxAbsoluteLeft; } } } else { // LTR case // Left-align the popup. left = element.getAbsoluteLeft(); // If the suggestion popup is not as wide as the text box, always // align to // the left edge of the text box. Otherwise, figure out whether to // left-align or right-align the popup. if (offsetWidthDiff > 0) { // Make sure scrolling is taken into account, since // box.getAbsoluteLeft() takes scrolling into account. int windowRight = Window.getClientWidth() + Window.getScrollLeft(); int windowLeft = Window.getScrollLeft(); // Distance from the left edge of the text box to the right edge // of the window int distanceToWindowRight = windowRight - left; // Distance from the left edge of the text box to the left edge // of the // window int distanceFromWindowLeft = left - windowLeft; // If there is not enough space for the overflow of the popup's // width to the right of hte text box, and there IS enough space // for the // overflow to the left of the text box, then right-align the // popup. // However, if there is not enough space on either side, then // stick with // left-alignment. if (distanceToWindowRight < offsetWidth && distanceFromWindowLeft >= offsetWidthDiff) { // Align with the right edge of the text box. left -= offsetWidthDiff; } } } // Calculate top position for the popup int top = element.getAbsoluteTop(); // Make sure scrolling is taken into account, since // box.getAbsoluteTop() takes scrolling into account. int windowTop = Window.getScrollTop(); int windowBottom = Window.getScrollTop() + Window.getClientHeight(); // Distance from the top edge of the window to the top edge of the // text box int distanceFromWindowTop = top - windowTop; // Distance from the bottom edge of the window to the bottom edge of // the text box int distanceToWindowBottom = windowBottom - (top + element.getOffsetHeight()); // If there is not enough space for the popup's height below the text // box and there IS enough space for the popup's height above the text // box, then then position the popup above the text box. However, if // there // is not enough space on either side, then stick with displaying the // popup below the text box. if (distanceToWindowBottom < offsetHeight && distanceFromWindowTop >= offsetHeight) { top -= offsetHeight; } else { // Position above the text box top += element.getOffsetHeight(); } hyperlinkTooltip.setPopupPosition(left, top); } public void setDisplayGridlines(boolean displayGridlines) { if (displayGridlines) { spreadsheet.removeClassName(NO_GRIDLINES_CLASSNAME); } else { spreadsheet.addClassName(NO_GRIDLINES_CLASSNAME); } if (loaded) { updateSheetPanePositions(); } } public void setDisplayRowColHeadings(boolean displayRowColHeadings) { this.displayRowColHeadings = displayRowColHeadings; if (displayRowColHeadings) { spreadsheet.removeClassName(NO_ROWCOLHEADINGS_CLASSNAME); } else { spreadsheet.addClassName(NO_ROWCOLHEADINGS_CLASSNAME); } if (loaded) { moveHeadersToMatchScroll(); updateSheetPanePositions(); updateColGrouping(); updateRowGrouping(); } } public void setVerticalSplitPosition(int verticalSplitPosition) { this.verticalSplitPosition = verticalSplitPosition; selectionWidget.setVerticalSplitPosition(verticalSplitPosition); } public void setHorizontalSplitPosition(int horizontalSplitPosition) { this.horizontalSplitPosition = horizontalSplitPosition; selectionWidget.setHorizontalSplitPosition(horizontalSplitPosition); if (horizontalSplitPosition > 0) { jsniUtil.replaceSelector(editedCellFreezeColumnStyle, "." + sheetId + " .top-left-pane .cell.col" + horizontalSplitPosition + ", ." + sheetId + " .bottom-left-pane .cell.col" + horizontalSplitPosition, 1); } else { jsniUtil.replaceSelector(editedCellFreezeColumnStyle, ".notusedselector", 1); } } protected Element getBottomRightPane() { return sheet; } protected Element getBottomLeftPane() { return bottomLeftPane; } protected Element getTopRightPane() { return topRightPane; } protected Element getTopLeftPane() { return topLeftPane; } public boolean isSheetElement(Element cast) { return cast == getBottomLeftPane() || cast == getTopLeftPane() || cast == getTopRightPane() || cast == getBottomRightPane(); } public void clearSelectedCellsOnCut() { actionHandler.clearSelectedCellsOnCut(); } public void refreshCellStyles() { clearBasicCellStyles(); updateCellStyles(); updateConditionalFormattingStyles(); ensureCellSelectionStyles(); } public boolean isTouchMode() { return touchMode; } public void setTouchMode(boolean touchMode) { this.touchMode = touchMode; } public void editCellComment(int col, int row) { col++; row++; String cellClassName = toKey(col, row); if (alwaysVisibleCellComments.containsKey(cellClassName)) { cellCommentEditMode = true; currentlyEditedCellComment = alwaysVisibleCellComments.get(cellClassName); currentlyEditedCellComment.setEditMode(true); } else { cellCommentEditMode = true; cellCommentCellColumn = col; cellCommentCellRow = row; showCellComment(col, row); currentlyEditedCellComment = cellCommentOverlay; cellCommentOverlay.setEditMode(true); } } public void commitComment(String text, int col, int row) { String cellClassName = toKey(col, row); cellCommentsMap.put(cellClassName, text); actionHandler.updateCellComment(text, col, row); } public boolean isSelectedCellPergentage() { CellData data = getCellData(getSelectedCellColumn(), getSelectedCellRow()); return data != null && data.isPercentage; } boolean isMac() { return isMac; } public void setFocused(final boolean focused) { if (focused) { removeStyleName("notfocused"); } else { addStyleName("notfocused"); } } public void setColGroupingData(List<GroupingData> data) { groupingDataCol = data; } public void setRowGroupingData(List<GroupingData> data) { groupingDataRow = data; } public void setColGroupingMax(int max) { colGroupMax = max; } public void setRowGroupingMax(int max) { rowGroupMax = max; } private void updateColGrouping() { updateGrouping(colGroupPane, colGroupFreezePane, groupingDataCol, frozenColumnHeaders, true, colGroupMax); int numberOfColGroups = displayRowColHeadings ? colGroupMax + 1 : colGroupMax; if (colGroupSummaryPane.getChildCount() == numberOfColGroups) { // nothings changed; don't re-draw return; } colGroupSummaryPane.removeAllChildren(); for (int i = 1; i <= numberOfColGroups; i++) { SpanElement text = Document.get().createSpanElement(); DivElement btn = Document.get().createDivElement(); colGroupSummaryPane.appendChild(btn); btn.appendChild(text); text.setInnerText(Integer.toString(i)); btn.setClassName("expandbutton"); final int level = i; Event.sinkEvents(btn, Event.ONCLICK); Event.setEventListener(btn, new EventListener() { @Override public void onBrowserEvent(Event event) { actionHandler.levelHeaderClicked(true, level); } }); } colGroupBorderPane.removeAllChildren(); for (int i = 1; i <= numberOfColGroups - 1; i++) { DivElement border = Document.get().createDivElement(); colGroupBorderPane.appendChild(border); border.setClassName("border"); border.getStyle().setMarginTop(18 * i, Unit.PX); } } private void updateRowGrouping() { updateGrouping(rowGroupPane, rowGroupFreezePane, groupingDataRow, frozenRowHeaders, false, rowGroupMax); int numberOfRowGroups = displayRowColHeadings ? rowGroupMax + 1 : rowGroupMax; if (rowGroupSummaryPane.getChildCount() == numberOfRowGroups) { // nothings changed; don't re-draw return; } rowGroupSummaryPane.removeAllChildren(); for (int i = 1; i <= numberOfRowGroups; i++) { DivElement btn = Document.get().createDivElement(); rowGroupSummaryPane.appendChild(btn); btn.setInnerText("" + i); btn.setClassName("expandbutton"); final int level = i; Event.sinkEvents(btn, Event.ONCLICK); Event.setEventListener(btn, new EventListener() { @Override public void onBrowserEvent(Event event) { actionHandler.levelHeaderClicked(false, level); } }); } rowGroupBorderPane.removeAllChildren(); for (int i = 1; i <= numberOfRowGroups - 1; i++) { DivElement border = Document.get().createDivElement(); rowGroupBorderPane.appendChild(border); border.setClassName("border"); border.getStyle().setMarginLeft(15 * i, Unit.PX); } } private void updateGrouping(DivElement groupPane, DivElement groupFreezePane, List<GroupingData> groupingDatas, ArrayList<DivElement> freezeHeaders, boolean useCol, int maxGrouping) { // remove old Iterator<Widget> iterator = iterator(); while (iterator.hasNext()) { Widget next = iterator.next(); if (next instanceof GroupingWidget) { orphan(next); } } groupPane.removeAllChildren(); groupFreezePane.removeAllChildren(); int START_PADDING = 0; // all markers start with a padding (inversed groups adds padding later) if (useCol && !colGroupInversed) { START_PADDING = 5; } else if (!useCol && !rowGroupInversed) { START_PADDING = 2; } if (maxGrouping > 0) { /* * Normal (non-inversed) groupings start at the 'startIndex' header * and continue to the 'endIndex' header, ending with an * expand/collapse button. The button is centered on the header * AFTER 'endIndex'. Additionally, the grouping has a small padding * (few pixels) in the start for visual purposes; it doesn't start * exactly between 'startindex-1' and 'startindex', but a couple of * pixels after. * * Inversed grouping do the same thing, but in reverse; the button * is in the middle of the cell BEFORE 'startIndex', from where the * grouping continues to 'endIndex', stopping a few pixels short of * the next cell. */ // starting pos offset if the other grouping pane is visible int startingOffset; if (useCol) { startingOffset = rowGroupPane.getClientWidth(); if (displayRowColHeadings) { startingOffset += getRowHeaderSize(); } } else { startingOffset = colGroupPane.getClientHeight(); if (displayRowColHeadings) { startingOffset += getColHeaderSize(); } } // all markers start with a padding (reverse adds padding later) startingOffset += START_PADDING; // For each grouping for (GroupingData data : groupingDatas) { GroupingWidget marker; if (useCol) { marker = new ColumnGrouping(data.uniqueIndex, actionHandler); marker.setInversed(colGroupInversed); } else { marker = new RowGrouping(data.uniqueIndex, actionHandler); marker.setInversed(rowGroupInversed); } /* find starting position */ // offset from left int pos = startingOffset; // add up header sizes before start index for (int index = 0; index < data.startIndex; index++) { if (useCol) { pos += getColumnWidth(index + 1); } else { pos += getRowHeight(index + 1); } } // inversed markers begin BEFORE the start index, so remove half // of the previous header if (marker.isInversed()) { if (useCol) { pos -= getColumnWidth(data.startIndex) / 2; } else { pos -= getRowHeight(data.startIndex) / 2; } } marker.setPos(pos, data.level - 1); marker.setCollapsed(data.collapsed); groupPane.appendChild(marker.getElement()); adopt(marker); /* calculate marker length */ double length = 0; // add each header between start and end index for (int col = data.startIndex; col <= data.endIndex; col++) { if (useCol) { length += getColumnWidth(col + 1); } else { length += getRowHeight(col + 1); } } /* calculate end position; center of next row/col */ // remove padding from beginning from the width. Also acts as // padding for inverted markers length -= START_PADDING; if (marker.isInversed()) { if (useCol) { length += getColumnWidth(data.startIndex) / 2d; } else { length += getRowHeight(data.startIndex) / 2d; } } else { if (useCol) { length += getColumnWidth(data.endIndex + 2) / 2d; } else { length += getRowHeight(data.endIndex + 2) / 2d; } } marker.setWidthPX(length); /* * If we have freeze panes, the marker needs to be present there * too. Easy fix; clone current marker and add it to freeze * pane. Positioning, length, etc. are already correct. */ if (freezeHeaders != null && freezeHeaders.size() > data.startIndex) { GroupingWidget clone = marker.cloneWidget(); groupFreezePane.appendChild(clone.getElement()); adopt(clone); } } } } /** * * @param index * 1 based column index * @return */ private int getColumnWidth(int index) { return actionHandler.getColWidthActual(index); } private void updateExtraCornerElements(int formulaBarHeight, int colGroupHeight, int rowGroupWidth) { groupingCorner.getStyle().setTop(formulaBarHeight, Unit.PX); if (rowGroupWidth == 0 || colGroupHeight == 0) { groupingCorner.getStyle().setDisplay(Display.NONE); } else { groupingCorner.getStyle().setDisplay(Display.BLOCK); } groupingCorner.getStyle().setHeight(colGroupHeight, Unit.PX); groupingCorner.getStyle().setWidth(rowGroupWidth, Unit.PX); Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { int topLeftPaneClientWidth = topLeftPane.getClientWidth(); int rowGroupPaneClientWidth = rowGroupPane.getClientWidth(); int width = topLeftPaneClientWidth + rowGroupPaneClientWidth; if (rowGroupPaneClientWidth == 0 && !isDisplayed(colGroupFreezePane)) { width -= 1; } else if (rowGroupPaneClientWidth != 0 && !isDisplayed(colGroupFreezePane)) { // NOOP } else if (rowGroupPaneClientWidth != 0 && isDisplayed(colGroupFreezePane)) { width += 2; } colGroupFreezePane.getStyle().setWidth(width, Unit.PX); int topLeftPaneClientHeight = topLeftPane.getClientHeight(); int colGroupPaneClientHeight = colGroupPane.getClientHeight(); int height = topLeftPaneClientHeight + colGroupPaneClientHeight; if (colGroupPaneClientHeight == 0 && !isDisplayed(rowGroupFreezePane)) { // NOOP } else if (colGroupPaneClientHeight != 0 && !isDisplayed(rowGroupFreezePane)) { height += 1; } else if (colGroupPaneClientHeight != 0 && isDisplayed(rowGroupFreezePane)) { height += 2; } rowGroupFreezePane.getStyle().setHeight(height, Unit.PX); // update grouping pane widths and heights // needs to be here since we need to know the frozen pane sizes int bottomLeftPaneClientHeight = bottomLeftPane.getClientHeight(); int topRightPaneClientWidth = topRightPane.getClientWidth(); int topPaneWidth = topRightPaneClientWidth + width; int leftPaneHeight = bottomLeftPaneClientHeight + height; if (isDisplayed(colGroupFreezePane)) { topPaneWidth += 1; } if (isDisplayed(rowGroupFreezePane)) { leftPaneHeight += 1; } colGroupPane.getStyle().setWidth(topPaneWidth, Unit.PX); colGroupBorderPane.getStyle().setWidth(topPaneWidth, Unit.PX); rowGroupPane.getStyle().setHeight(leftPaneHeight, Unit.PX); rowGroupBorderPane.getStyle().setHeight(leftPaneHeight, Unit.PX); } }); } private boolean isDisplayed(Element element) { return !Display.NONE.getCssName().equals(element.getStyle().getDisplay()); } private int updateExtraColumnHeaderElements(int formulaBarHeight) { groupingCorner.getStyle().setTop(formulaBarHeight, Unit.PX); // calculate grouping element sizes int colGroupHeight = 0; if (colGroupMax > 0) { int numberOfColGroups = displayRowColHeadings ? colGroupMax + 1 : colGroupMax; colGroupHeight = ColumnGrouping.getTotalHeight(numberOfColGroups); } int rowGroupWidth = 0; if (rowGroupMax > 0) { rowGroupWidth = ColumnGrouping.getTotalWidth(rowGroupMax + 1); } // grouping element sizing if (colGroupHeight == 0) { colGroupPane.getStyle().setDisplay(Display.NONE); colGroupSummaryPane.getStyle().setDisplay(Display.NONE); } else { colGroupPane.getStyle().setDisplay(Display.BLOCK); colGroupSummaryPane.getStyle().setDisplay(Display.BLOCK); } if (!displayRowColHeadings) { colGroupSummaryPane.getStyle().setDisplay(Display.NONE); } if (frozenColumnHeaders != null && colGroupMax > 0) { colGroupFreezePane.getStyle().setDisplay(Display.BLOCK); } else { colGroupFreezePane.getStyle().setDisplay(Display.NONE); } colGroupPane.getStyle().setHeight(colGroupHeight, Unit.PX); colGroupPane.getStyle().setTop(formulaBarHeight, Unit.PX); colGroupFreezePane.getStyle().setHeight(colGroupHeight, Unit.PX); colGroupFreezePane.getStyle().setTop(formulaBarHeight, Unit.PX); colGroupSummaryPane.getStyle().setTop(formulaBarHeight, Unit.PX); colGroupSummaryPane.getStyle().setHeight(colGroupHeight, Unit.PX); if (loaded) { colGroupSummaryPane.getStyle().setWidth(getRowHeaderSize(), Unit.PX); } colGroupSummaryPane.getStyle().setLeft(rowGroupWidth, Unit.PX); colGroupBorderPane.getStyle().setTop(formulaBarHeight, Unit.PX); colGroupBorderPane.getStyle().setLeft(rowGroupWidth, Unit.PX); colGroupBorderPane.getStyle().setHeight(colGroupHeight, Unit.PX); rowGroupBorderPane.getStyle().setTop(formulaBarHeight + colGroupHeight, Unit.PX); rowGroupBorderPane.getStyle().setLeft(0, Unit.PX); rowGroupBorderPane.getStyle().setWidth(rowGroupWidth, Unit.PX); calculatedRowGroupWidth = rowGroupWidth; calculatedColGroupHeight = colGroupHeight; return colGroupHeight; } private int updateExtraRowHeaderElements(int formulaBarHeight) { // calculate grouping element sizes int colGroupHeight = 0; if (colGroupMax > 0) { int numberOfColGroups = displayRowColHeadings ? colGroupMax + 1 : colGroupMax; colGroupHeight = GroupingWidget.getTotalHeight(numberOfColGroups); } int rowGroupWidth = 0; if (rowGroupMax > 0) { int numberOfRowGroups = displayRowColHeadings ? rowGroupMax + 1 : rowGroupMax; rowGroupWidth = GroupingWidget.getTotalWidth(numberOfRowGroups); } if (rowGroupWidth == 0) { rowGroupPane.getStyle().setDisplay(Display.NONE); rowGroupSummaryPane.getStyle().setDisplay(Display.NONE); } else { rowGroupPane.getStyle().setDisplay(Display.BLOCK); rowGroupSummaryPane.getStyle().setDisplay(Display.BLOCK); } if (!displayRowColHeadings) { rowGroupSummaryPane.getStyle().setDisplay(Display.NONE); } if (frozenRowHeaders != null && rowGroupMax > 0) { rowGroupFreezePane.getStyle().setDisplay(Display.BLOCK); } else { rowGroupFreezePane.getStyle().setDisplay(Display.NONE); } // grouping element sizing rowGroupPane.getStyle().setWidth(rowGroupWidth, Unit.PX); rowGroupPane.getStyle().setTop(formulaBarHeight, Unit.PX); rowGroupFreezePane.getStyle().setWidth(rowGroupWidth, Unit.PX); rowGroupFreezePane.getStyle().setTop(formulaBarHeight, Unit.PX); rowGroupSummaryPane.getStyle().setTop(formulaBarHeight + colGroupHeight, Unit.PX); if (loaded) { rowGroupSummaryPane.getStyle().setHeight(getColHeaderSize(), Unit.PX); rowGroupSummaryPane.getStyle().setLineHeight(getColHeaderSize(), Unit.PX); } rowGroupSummaryPane.getStyle().setWidth(rowGroupWidth, Unit.PX); return rowGroupWidth; } public void setColGroupingInversed(boolean inversed) { colGroupInversed = inversed; } public void setRowGroupingInversed(boolean inversed) { rowGroupInversed = inversed; } public void setInvalidFormulaMessage(String invalidFormulaMessage) { this.invalidFormulaMessage = invalidFormulaMessage; updateAllVisibleComments(); } }