com.vaadin.flow.component.spreadsheet.Spreadsheet Maven / Gradle / Ivy
/**
* Copyright 2000-2024 Vaadin Ltd.
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See {@literal } for the full
* license.
*/
package com.vaadin.flow.component.spreadsheet;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.formula.BaseFormulaEvaluator;
import org.apache.poi.ss.formula.ConditionalFormattingEvaluator;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.ClientAnchor.AnchorType;
import org.apache.poi.ss.usermodel.Comment;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Hyperlink;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.SheetVisibility;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeUtil;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.ss.util.PaneInformation;
import org.apache.poi.ss.util.WorkbookUtil;
import org.apache.poi.util.Units;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFHyperlink;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.xmlbeans.impl.values.XmlValueDisconnectedException;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.DomEvent;
import com.vaadin.flow.component.EventData;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.charts.Chart;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.spreadsheet.SheetOverlayWrapper.OverlayChangeListener;
import com.vaadin.flow.component.spreadsheet.action.SpreadsheetDefaultActionHandler;
import com.vaadin.flow.component.spreadsheet.client.CellData;
import com.vaadin.flow.component.spreadsheet.client.MergedRegion;
import com.vaadin.flow.component.spreadsheet.client.MergedRegionUtil.MergedRegionContainer;
import com.vaadin.flow.component.spreadsheet.client.OverlayInfo;
import com.vaadin.flow.component.spreadsheet.client.SpreadsheetActionDetails;
import com.vaadin.flow.component.spreadsheet.command.SizeChangeCommand;
import com.vaadin.flow.component.spreadsheet.command.SizeChangeCommand.Type;
import com.vaadin.flow.component.spreadsheet.framework.Action;
import com.vaadin.flow.component.spreadsheet.framework.ReflectTools;
import com.vaadin.flow.component.spreadsheet.rpc.SpreadsheetClientRpc;
import com.vaadin.flow.component.spreadsheet.shared.GroupingData;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.shared.Registration;
import com.vaadin.pro.licensechecker.LicenseChecker;
import elemental.json.JsonValue;
/**
* Vaadin Spreadsheet is a component which allows displaying and interacting
* with the contents of an Excel file. The Spreadsheet can be used in any Vaadin
* application for enabling users to view and manipulate Excel files in their
* web browsers.
*
* @author Vaadin Ltd.
*/
@Tag("vaadin-spreadsheet")
@JsModule("./vaadin-spreadsheet/vaadin-spreadsheet.js")
// Need an explicit reference to Chart class, since the DefaultChartCreator
// class that references it is only accessed through reflection
@Uses(Chart.class)
@SuppressWarnings("serial")
public class Spreadsheet extends Component
implements HasSize, HasStyle, Action.Container {
private static final Logger LOGGER = LoggerFactory
.getLogger(Spreadsheet.class);
static {
VaadinService service = VaadinService.getCurrent();
Properties properties = new Properties();
try {
properties.load(Spreadsheet.class
.getResourceAsStream("spreadsheet.properties"));
} catch (Exception e) {
LOGGER.warn("Unable to read Spreadsheet properties file", e);
throw new ExceptionInInitializerError(e);
}
String version = properties.getProperty("spreadsheet.version");
if (service != null) {
if (!service.getDeploymentConfiguration().isProductionMode()) {
LicenseChecker.checkLicenseFromStaticBlock(
"vaadin-spreadsheet-flow", version);
}
}
}
@Override
public void setId(String id) {
getElement().setProperty("id", id);
}
// from SaredState
// private Map resources = new HashMap<>();
private Map resources = new HashMap<>();
// spreadsheetState
private int rowBufferSize = 200;
private int columnBufferSize = 200;
private int rows;
private int cols;
private List colGroupingData;
private int colGroupingMax;
private int rowGroupingMax;
private boolean colGroupingInversed;
private boolean rowGroupingInversed;
private float defRowH;
private int defColW;
private float[] rowH;
private int[] colW;
/** should the sheet be reloaded on client side */
private boolean reload;
/** 1-based */
private int sheetIndex = 1;
private String[] sheetNames = null;
protected HashMap cellStyleToCSSStyle = null;
private HashMap rowIndexToStyleIndex = null;
private HashMap columnIndexToStyleIndex = null;
private Set lockedColumnIndexes = null;
private Set lockedRowIndexes = null;
private ArrayList shiftedCellBorderStyles = null;
/**
* All conditional formatting styles for this sheet.
*/
private HashMap conditionalFormattingStyles = null;
/** 1-based */
private ArrayList hiddenColumnIndexes = null;
/** 1-based */
private ArrayList hiddenRowIndexes = null;
private int[] verticalScrollPositions;
private int[] horizontalScrollPositions;
private boolean sheetProtected;
private HashMap cellKeysToEditorIdMap;
private HashMap componentIDtoCellKeysMap;
// Cell CSS key to link tooltip (usually same as address)
private HashMap hyperlinksTooltips;
private HashMap cellComments;
private HashMap cellCommentAuthors;
private ArrayList visibleCellComments;
private Set invalidFormulaCells;
private HashMap overlays;
private ArrayList mergedRegions;
private int verticalSplitPosition = 0;
private int horizontalSplitPosition = 0;
private String infoLabelValue;
private boolean workbookChangeToggle;
private Locale locale;
int getCols() {
return cols;
}
private List getColGroupingData() {
return colGroupingData;
}
int getColGroupingMax() {
return colGroupingMax;
}
int getRowGroupingMax() {
return rowGroupingMax;
}
boolean isColGroupingInversed() {
return colGroupingInversed;
}
boolean isRowGroupingInversed() {
return rowGroupingInversed;
}
float getDefRowH() {
return defRowH;
}
int getDefColW() {
return defColW;
}
private float[] getRowH() {
return rowH;
}
int[] getColW() {
return colW;
}
private int getSheetIndex() {
return sheetIndex;
}
String[] getSheetNames() {
return sheetNames;
}
HashMap getCellStyleToCSSStyle() {
return cellStyleToCSSStyle;
}
HashMap getRowIndexToStyleIndex() {
return rowIndexToStyleIndex;
}
HashMap getColumnIndexToStyleIndex() {
return columnIndexToStyleIndex;
}
Set getLockedColumnIndexes() {
return lockedColumnIndexes;
}
Set getLockedRowIndexes() {
return lockedRowIndexes;
}
ArrayList getShiftedCellBorderStyles() {
return shiftedCellBorderStyles;
}
HashMap getConditionalFormattingStyles() {
return conditionalFormattingStyles;
}
private ArrayList getHiddenColumnIndexes() {
return hiddenColumnIndexes;
}
private ArrayList getHiddenRowIndexes() {
return hiddenRowIndexes;
}
int[] getVerticalScrollPositions() {
return verticalScrollPositions;
}
int[] getHorizontalScrollPositions() {
return horizontalScrollPositions;
}
private boolean isSheetProtected() {
return sheetProtected;
}
private HashMap getCellKeysToEditorIdMap() {
return cellKeysToEditorIdMap;
}
HashMap getComponentIDtoCellKeysMap() {
return componentIDtoCellKeysMap;
}
private HashMap getHyperlinksTooltips() {
return hyperlinksTooltips;
}
private HashMap getCellComments() {
return cellComments;
}
private HashMap getCellCommentAuthors() {
return cellCommentAuthors;
}
private ArrayList getVisibleCellComments() {
return visibleCellComments;
}
private Set getInvalidFormulaCells() {
return invalidFormulaCells;
}
private HashMap getOverlays() {
return overlays;
}
private ArrayList getMergedRegions() {
return mergedRegions;
}
private int getVerticalSplitPosition() {
return verticalSplitPosition;
}
private int getHorizontalSplitPosition() {
return horizontalSplitPosition;
}
private String getInfoLabelValue() {
return infoLabelValue;
}
private boolean isWorkbookChangeToggle() {
return workbookChangeToggle;
}
void setRows(int rows) {
this.rows = rows;
getElement().setProperty("rows", rows);
}
void setCols(int cols) {
this.cols = cols;
getElement().setProperty("cols", cols);
}
void setColGroupingData(List colGroupingData) {
this.colGroupingData = colGroupingData;
getElement().setProperty("colGroupingData",
Serializer.serialize(colGroupingData));
}
void setRowGroupingData(List rowGroupingData) {
getElement().setProperty("rowGroupingData",
Serializer.serialize(rowGroupingData));
}
void setColGroupingMax(int colGroupingMax) {
this.colGroupingMax = colGroupingMax;
getElement().setProperty("colGroupingMax", colGroupingMax);
}
void setRowGroupingMax(int rowGroupingMax) {
this.rowGroupingMax = rowGroupingMax;
getElement().setProperty("rowGroupingMax", rowGroupingMax);
}
void setColGroupingInversed(boolean colGroupingInversed) {
this.colGroupingInversed = colGroupingInversed;
getElement().setProperty("colGroupingInversed", colGroupingInversed);
}
void setRowGroupingInversed(boolean rowGroupingInversed) {
this.rowGroupingInversed = rowGroupingInversed;
getElement().setProperty("rowGroupingInversed", rowGroupingInversed);
}
void setDefRowH(float defRowH) {
this.defRowH = defRowH;
getElement().setProperty("defRowH", defRowH);
}
void setDefColW(int defColW) {
this.defColW = defColW;
getElement().setProperty("defColW", defColW);
}
void setRowH(float[] rowH) {
this.rowH = rowH;
getElement().setProperty("rowH", Serializer.serialize(rowH));
}
void setColW(int[] colW) {
this.colW = colW;
getElement().setProperty("colW", Serializer.serialize(colW));
}
private void setReload(boolean reload) {
if (reload)
getElement().setProperty("reload", System.currentTimeMillis());
}
private void setSheetIndex(int sheetIndex) {
this.sheetIndex = sheetIndex;
getElement().setProperty("sheetIndex", sheetIndex);
}
private void setSheetNames(String[] sheetNames) {
this.sheetNames = sheetNames;
getElement().setProperty("sheetNames",
Serializer.serialize(sheetNames));
}
void setCellStyleToCSSStyle(HashMap cellStyleToCSSStyle) {
this.cellStyleToCSSStyle = cellStyleToCSSStyle;
getElement().setProperty("cellStyleToCSSStyle",
Serializer.serialize(cellStyleToCSSStyle));
}
void setRowIndexToStyleIndex(
HashMap rowIndexToStyleIndex) {
this.rowIndexToStyleIndex = rowIndexToStyleIndex;
getElement().setProperty("rowIndexToStyleIndex",
Serializer.serialize(rowIndexToStyleIndex));
}
void setColumnIndexToStyleIndex(
HashMap columnIndexToStyleIndex) {
this.columnIndexToStyleIndex = columnIndexToStyleIndex;
getElement().setProperty("columnIndexToStyleIndex",
Serializer.serialize(columnIndexToStyleIndex));
}
void setLockedColumnIndexes(Set lockedColumnIndexes) {
this.lockedColumnIndexes = lockedColumnIndexes;
getElement().setProperty("lockedColumnIndexes",
Serializer.serialize(lockedColumnIndexes));
}
void setLockedRowIndexes(Set lockedRowIndexes) {
this.lockedRowIndexes = lockedRowIndexes;
getElement().setProperty("lockedRowIndexes",
Serializer.serialize(lockedRowIndexes));
}
void setShiftedCellBorderStyles(ArrayList shiftedCellBorderStyles) {
this.shiftedCellBorderStyles = shiftedCellBorderStyles;
getElement().setProperty("shiftedCellBorderStyles",
Serializer.serialize(shiftedCellBorderStyles));
}
void setConditionalFormattingStyles(
HashMap conditionalFormattingStyles) {
this.conditionalFormattingStyles = conditionalFormattingStyles;
getElement().setProperty("conditionalFormattingStyles",
Serializer.serialize(conditionalFormattingStyles));
}
void setHiddenColumnIndexes(ArrayList hiddenColumnIndexes) {
this.hiddenColumnIndexes = hiddenColumnIndexes;
getElement().setProperty("hiddenColumnIndexes",
Serializer.serialize(hiddenColumnIndexes));
}
void setHiddenRowIndexes(ArrayList hiddenRowIndexes) {
this.hiddenRowIndexes = hiddenRowIndexes;
getElement().setProperty("hiddenRowIndexes",
Serializer.serialize(hiddenRowIndexes));
}
void setVerticalScrollPositions(int[] verticalScrollPositions) {
this.verticalScrollPositions = verticalScrollPositions;
getElement().setProperty("verticalScrollPositions",
Serializer.serialize(verticalScrollPositions));
}
void setHorizontalScrollPositions(int[] horizontalScrollPositions) {
this.horizontalScrollPositions = horizontalScrollPositions;
getElement().setProperty("horizontalScrollPositions",
Serializer.serialize(horizontalScrollPositions));
}
private void setSheetProtected(boolean sheetProtected) {
this.sheetProtected = sheetProtected;
getElement().setProperty("sheetProtected", sheetProtected);
}
private void setWorkbookProtected(boolean workbookProtected) {
getElement().setProperty("workbookProtected", workbookProtected);
}
private void setCellKeysToEditorIdMap(
HashMap cellKeysToEditorIdMap) {
this.cellKeysToEditorIdMap = cellKeysToEditorIdMap;
getElement().setProperty("cellKeysToEditorIdMap",
Serializer.serialize(cellKeysToEditorIdMap));
}
private void setComponentIDtoCellKeysMap(
HashMap componentIDtoCellKeysMap) {
this.componentIDtoCellKeysMap = componentIDtoCellKeysMap;
getElement().setProperty("componentIDtoCellKeysMap",
Serializer.serialize(componentIDtoCellKeysMap));
}
private void setHyperlinksTooltips(
HashMap hyperlinksTooltips) {
this.hyperlinksTooltips = hyperlinksTooltips;
getElement().setProperty("hyperlinksTooltips",
Serializer.serialize(hyperlinksTooltips));
}
private void setCellComments(HashMap cellComments) {
this.cellComments = cellComments;
getElement().setProperty("cellComments",
Serializer.serialize(cellComments));
}
private void setCellCommentAuthors(
HashMap cellCommentAuthors) {
this.cellCommentAuthors = cellCommentAuthors;
getElement().setProperty("cellCommentAuthors",
Serializer.serialize(cellCommentAuthors));
}
private void setVisibleCellComments(ArrayList visibleCellComments) {
this.visibleCellComments = visibleCellComments;
getElement().setProperty("visibleCellComments",
Serializer.serialize(visibleCellComments));
}
private void setInvalidFormulaCells(Set invalidFormulaCells) {
this.invalidFormulaCells = invalidFormulaCells;
getElement().setProperty("invalidFormulaCells",
Serializer.serialize(invalidFormulaCells));
}
private void setHasActions(boolean hasActions) {
getElement().setProperty("hasActions", hasActions);
}
private void setOverlays(HashMap overlays) {
this.overlays = overlays;
getElement().setProperty("overlays", Serializer.serialize(overlays));
}
void setMergedRegions(ArrayList mergedRegions) {
this.mergedRegions = mergedRegions;
getElement().setProperty("mergedRegions",
Serializer.serialize(mergedRegions));
}
private void setDisplayGridlines(boolean displayGridlines) {
getElement().setProperty("displayGridlines", displayGridlines);
}
private void setDisplayRowColHeadings(boolean displayRowColHeadings) {
getElement().setProperty("displayRowColHeadings",
displayRowColHeadings);
}
void setVerticalSplitPosition(int verticalSplitPosition) {
this.verticalSplitPosition = verticalSplitPosition;
getElement().setProperty("verticalSplitPosition",
verticalSplitPosition);
}
void setHorizontalSplitPosition(int horizontalSplitPosition) {
this.horizontalSplitPosition = horizontalSplitPosition;
getElement().setProperty("horizontalSplitPosition",
horizontalSplitPosition);
}
private void setInfoLabelValue(String infoLabelValue) {
this.infoLabelValue = infoLabelValue;
getElement().setProperty("infoLabelValue", infoLabelValue);
}
private void setWorkbookChangeToggle(boolean workbookChangeToggle) {
this.workbookChangeToggle = workbookChangeToggle;
getElement().setProperty("workbookChangeToggle", workbookChangeToggle);
}
void setLockFormatColumns(boolean lockFormatColumns) {
getElement().setProperty("lockFormatColumns", lockFormatColumns);
}
void setLockFormatRows(boolean lockFormatRows) {
getElement().setProperty("lockFormatRows", lockFormatRows);
}
void setNamedRanges(List namedRanges) {
getElement().setProperty("namedRanges",
Serializer.serialize(namedRanges));
}
void onPopupButtonClick(int row, int column) {
PopupButton popup = sheetPopupButtons
.get(SpreadsheetUtil.relativeToAbsolute(this,
new CellReference(row - 1, column - 1)));
if (popup != null) {
popup.openPopup();
}
}
void onPopupClose(int row, int column) {
PopupButton popup = sheetPopupButtons
.get(SpreadsheetUtil.relativeToAbsolute(this,
new CellReference(row - 1, column - 1)));
if (popup != null) {
popup.closePopup();
}
}
/*
* CLIENT RPC
*/
// LOOK FOR THIS INSIDE CLIENTRPC VARIABLE
/*
* SERVER RPC
*/
@DomEvent("spreadsheet-event")
public static class SpreadsheetEvent extends ComponentEvent {
private final String type;
private final JsonValue data;
public SpreadsheetEvent(Spreadsheet source, boolean fromClient,
@EventData("event.detail.type") String type,
@EventData("event.detail.data") JsonValue data) {
super(source, fromClient);
this.type = type;
this.data = data;
}
public String getType() {
return type;
}
public JsonValue getData() {
return data;
}
}
/*
* END OF FLOW RELATED STUFF
*/
/**
* This is a style which hides the top (address and formula) bar.
*/
public static final String HIDE_FUNCTION_BAR_STYLE = "hidefunctionbar";
/**
* This is a style which hides the bottom (sheet selection) bar.
*/
public static final String HIDE_TABSHEET_STYLE = "hidetabsheet";
/**
* A common formula evaluator for this Spreadsheet
*/
private FormulaEvaluator formulaEvaluator;
/**
* A common conditional formatting formula evaluator for this Spreadsheet
* needed for proper value string conversions
*/
private ConditionalFormattingEvaluator conditionalFormattingEvaluator;
/**
* Pixel width of the filter popup button
*/
private static final int FILTER_BUTTON_PIXEL_WIDTH = 14;
/**
* Extra padding (in pixels) to add between the filter popup button and cell
* text when autofitting a column.
*/
private static final int FILTER_BUTTON_PIXEL_PADDING = 2;
/**
* Map of autofitted column widths in points
*/
private Map autofittedColumnWidths = new WeakHashMap<>();
private SpreadsheetClientRpc clientRpc = new SpreadsheetClientRpc() {
@Override
public void updateBottomRightCellValues(ArrayList cellData) {
getElement().callJsFunction("updateBottomRightCellValues",
Serializer.serialize(cellData));
}
@Override
public void updateTopLeftCellValues(ArrayList cellData) {
getElement().callJsFunction("updateTopLeftCellValues",
Serializer.serialize(cellData));
}
@Override
public void updateTopRightCellValues(ArrayList cellData) {
getElement().callJsFunction("updateTopRightCellValues",
Serializer.serialize(cellData));
}
@Override
public void updateBottomLeftCellValues(ArrayList cellData) {
getElement().callJsFunction("updateBottomLeftCellValues",
Serializer.serialize(cellData));
}
@Override
public void updateFormulaBar(String possibleName, int col, int row) {
getElement().executeJs(
"queueMicrotask(() => this.updateFormulaBar($0, $1, $2));",
possibleName, col, row);
}
@Override
public void invalidCellAddress() {
getElement().callJsFunction("invalidCellAddress");
}
@Override
public void showSelectedCell(String name, int col, int row,
String cellValue, boolean function, boolean locked,
boolean initialSelection) {
selectionManager.onCellSelected(row, col, initialSelection);
getElement().callJsFunction("showSelectedCell", name, col, row,
cellValue, function, locked, initialSelection);
}
@Override
public void showActions(
ArrayList actionDetails) {
getElement().callJsFunction("showActions",
Serializer.serialize(actionDetails));
}
@Override
public void setSelectedCellAndRange(String name, int col, int row,
int c1, int c2, int r1, int r2, boolean scroll) {
getElement().callJsFunction("setSelectedCellAndRange", name, col,
row, c1, c2, r1, r2, scroll);
}
@Override
public void cellsUpdated(ArrayList cellData) {
getElement().callJsFunction("cellsUpdated",
Serializer.serialize(cellData));
}
@Override
public void refreshCellStyles() {
getElement().callJsFunction("refreshCellStyles");
}
@Override
public void editCellComment(int col, int row) {
getElement().callJsFunction("editCellComment", col, row);
}
};
/**
* An interface for handling the edited cell value from user input.
*/
public interface CellValueHandler extends Serializable {
/**
* Called if a cell value has been edited by the user by using the
* default cell editor. Use
* {@link Spreadsheet#setCellValueHandler(CellValueHandler)} to enable
* it for the spreadsheet.
*
* @param cell
* The cell that has been edited, may be null
if
* the cell doesn't yet exists
* @param sheet
* The sheet the cell belongs to, the currently active sheet
* @param colIndex
* Cell column index, 0-based
* @param rowIndex
* Cell row index, 0-based
* @param newValue
* The value user has entered
* @param formulaEvaluator
* The {@link FormulaEvaluator} for this sheet
* @param formatter
* The {@link DataFormatter} for this workbook
* @param conditionalFormattingEvaluator
* The {@link ConditionalFormattingEvaluator} for this
* workbook
* @return true
if component default parsing should still
* be done, false
if not
*/
public boolean cellValueUpdated(Cell cell, Sheet sheet, int colIndex,
int rowIndex, String newValue,
FormulaEvaluator formulaEvaluator, DataFormatter formatter,
ConditionalFormattingEvaluator conditionalFormattingEvaluator);
}
/**
* An interface for handling cell deletion from user input.
*/
public interface CellDeletionHandler extends Serializable {
/**
* Called if a cell value has been deleted by the user. Use
* {@link Spreadsheet#setCellDeletionHandler(CellDeletionHandler)} to
* enable it for the spreadsheet.
*
* @param cell
* The cell that has been deleted
* @param sheet
* The sheet the cell belongs to, the currently active sheet
* @param colIndex
* Cell column index, 0-based
* @param rowIndex
* Cell row index, 0-based
* @param formulaEvaluator
* The {@link FormulaEvaluator} for this sheet
* @param formatter
* The {@link DataFormatter} for this workbook
* @param conditionalFormattingEvaluator
* The {@link ConditionalFormattingEvaluator} for this
* workbook
* @return true
if component default deletion should still
* be done, false
if not
*/
public boolean cellDeleted(Cell cell, Sheet sheet, int colIndex,
int rowIndex, FormulaEvaluator formulaEvaluator,
DataFormatter formatter,
ConditionalFormattingEvaluator conditionalFormattingEvaluator);
/**
* Called if individually selected cell values have been deleted by the
* user. Use
* {@link Spreadsheet#setCellDeletionHandler(CellDeletionHandler)} to
* enable it for the spreadsheet.
*
* @param individualSelectedCells
* The cells that have been deleted
* @param sheet
* The sheet the cells belong to, the currently active sheet
* @param formulaEvaluator
* The {@link FormulaEvaluator} for this sheet
* @param formatter
* The {@link DataFormatter} for this workbook
* @param conditionalFormattingEvaluator
* The {@link ConditionalFormattingEvaluator} for this
* workbook
* @return true
if component default deletion should still
* be done, false
if not
*/
public boolean individualSelectedCellsDeleted(
List individualSelectedCells, Sheet sheet,
FormulaEvaluator formulaEvaluator, DataFormatter formatter,
ConditionalFormattingEvaluator conditionalFormattingEvaluator);
/**
* Called if a cell range has been deleted by the user. Use
* {@link Spreadsheet#setCellDeletionHandler(CellDeletionHandler)} to
* enable it for the spreadsheet.
*
* @param cellRangeAddresses
* The range of cells that has been deleted
* @param sheet
* The sheet the cells belongs to, the currently active sheet
* @param formulaEvaluator
* The {@link FormulaEvaluator} for this sheet
* @param formatter
* The {@link DataFormatter} for this workbook
* @param conditionalFormattingEvaluator
* The {@link ConditionalFormattingEvaluator} for this
* workbook
* @return true
if component default deletion should still
* be done, false
if not
*/
public boolean cellRangeDeleted(
List cellRangeAddresses, Sheet sheet,
FormulaEvaluator formulaEvaluator, DataFormatter formatter,
ConditionalFormattingEvaluator conditionalFormattingEvaluator);
}
/**
* An interface for handling clicks on cells that contain a hyperlink.
*
* Implement this interface and set it with
* {@link Spreadsheet#setHyperlinkCellClickHandler(HyperlinkCellClickHandler)}
* to customize the default behavior.
*/
public interface HyperlinkCellClickHandler extends Serializable {
/**
* Called when a hyperlink cell has been clicked.
*
* Assumes the implementation knows which spreadsheet is in use if
* needed, and how to navigate or perform some other action.
*
* @param cell
* The cell that contains the hyperlink
* @param hyperlink
* The actual hyperlink
*/
public void onHyperLinkCellClick(Cell cell, Hyperlink hyperlink);
/**
* @return link target for use as a tooltip
*/
public String getHyperlinkFunctionTarget(Cell cell);
}
private SpreadsheetStyleFactory styler;
private HyperlinkCellClickHandler hyperlinkCellClickHandler;
private SpreadsheetComponentFactory customComponentFactory;
private final CellSelectionManager selectionManager = new CellSelectionManager(
this);
private final CellSelectionShifter cellShifter = new CellSelectionShifter(
this);
private final ContextMenuManager contextMenuManager = new ContextMenuManager(
this);
private final SpreadsheetHistoryManager historyManager = new SpreadsheetHistoryManager(
this);
private ConditionalFormatter conditionalFormatter;
/**
* caches data, so it needs to be stable for the life of a given workbook
*/
private CellValueManager valueManager;
/** The first visible row in the scroll area **/
private int firstRow;
/** The last visible row in the scroll area **/
private int lastRow;
/** The first visible column in the scroll area **/
private int firstColumn;
/** The last visible column in the scroll area **/
private int lastColumn;
private boolean chartsEnabled = true;
/**
* This is used for making sure the cells are sent to client side in when
* the next cell data request comes. This is triggered when the client side
* connector init() method is run.
*/
private boolean reloadCellDataOnNextScroll;
private int defaultNewSheetRows = SpreadsheetFactory.DEFAULT_ROWS;
private int defaultNewSheetColumns = SpreadsheetFactory.DEFAULT_COLUMNS;
private boolean topLeftCellCommentsLoaded;
private SpreadsheetDefaultActionHandler defaultActionHandler;
protected int mergedRegionCounter;
private Workbook workbook;
/** are tables for currently active sheet loaded */
private boolean tablesLoaded;
private SheetState sheetState = new SheetState(this);
/** image sizes need to be recalculated on column/row resizing */
private boolean reloadImageSizesFromPOI;
private String defaultPercentageFormat = "0.00%";
protected String initialSheetSelection = null;
private Set customComponents = new HashSet();
private Map sheetPopupButtons = new HashMap();
private HashSet attachedPopupButtons = new HashSet();
/**
* Set of images contained in the currently active sheet.
*/
private HashSet sheetOverlays;
private Set overlayComponents = new HashSet();
private HashSet tables;
private final Map> invalidFormulas = new HashMap>();
/**
* Container for merged regions for the currently active sheet.
*/
protected final MergedRegionContainer mergedRegionContainer = new MergedRegionContainer() {
/*
* (non-Javadoc)
*
* @see com.vaadin.flow.component.spreadsheet.client.MergedRegionUtil.
* MergedRegionContainer#getMergedRegionStartingFrom(int, int)
*/
@Override
public MergedRegion getMergedRegionStartingFrom(int column, int row) {
List mergedRegions = getMergedRegions();
if (mergedRegions != null) {
for (MergedRegion region : mergedRegions) {
if (region.col1 == column && region.row1 == row) {
return region;
}
}
}
return null;
}
/*
* (non-Javadoc)
*
* @see com.vaadin.flow.component.spreadsheet.client.MergedRegionUtil.
* MergedRegionContainer#getMergedRegion(int, int)
*/
@Override
public MergedRegion getMergedRegion(int column, int row) {
List mergedRegions = getMergedRegions();
if (mergedRegions != null) {
for (MergedRegion region : mergedRegions) {
if (region.col1 <= column && region.row1 <= row
&& region.col2 >= column && region.row2 >= row) {
return region;
}
}
}
return null;
}
};
private Set rowsWithComponents;
/**
* Minimum row height for rows containing components (in points).
*/
private int minimumRowHeightForComponents = 30;
/**
* Creates a new Spreadsheet component using the newer Excel version format
* {@link XSSFWorkbook}. Also creates one sheet using the default row
* {@link SpreadsheetFactory#DEFAULT_ROWS} and column
* {@link SpreadsheetFactory#DEFAULT_COLUMNS} counts.
*/
public Spreadsheet() {
this(SpreadsheetFactory.DEFAULT_ROWS,
SpreadsheetFactory.DEFAULT_COLUMNS);
}
/**
* Creates a new Spreadsheet component using the newer Excel version format
* {@link XSSFWorkbook}. Also creates one sheet using the given row and
* column counts. These counts will also be set as default for any new
* sheets created later.
*
* @param defaultRowCount
* Default row count for new sheets
* @param defaultColumnCount
* Default column count for new sheets
*/
public Spreadsheet(int defaultRowCount, int defaultColumnCount) {
// getUI().ifPresent(ui ->
// ui.getPage().addDynamicImport("spreadsheet-lit-element/vaadin-spreadsheet.js"));
init();
setDefaultRowCount(defaultRowCount);
setDefaultColumnCount(defaultColumnCount);
SpreadsheetFactory.loadSpreadsheetWith(this, null, getDefaultRowCount(),
getDefaultColumnCount());
}
/**
* Creates a new Spreadsheet component and loads the given Workbook.
*
* @param workbook
* Workbook to load
*/
public Spreadsheet(Workbook workbook) {
init();
SpreadsheetFactory.loadSpreadsheetWith(this, workbook,
getDefaultRowCount(), getDefaultColumnCount());
}
/**
* Creates a new Spreadsheet component and loads the given Excel file.
*
* @param file
* Excel file
* @throws IOException
* If file has invalid format or there is no access to the file
*/
public Spreadsheet(File file) throws IOException {
init();
SpreadsheetFactory.reloadSpreadsheetComponent(this, file);
}
/**
* Creates a new Spreadsheet component based on the given input stream. The
* expected format is that of an Excel file.
*
* @param inputStream
* Stream that provides Excel-formatted data.
* @throws IOException
* If there is an error handling the stream, or if the data is
* in an invalid format.
*/
public Spreadsheet(InputStream inputStream) throws IOException {
init();
SpreadsheetFactory.reloadSpreadsheetComponent(this, inputStream);
}
private void init() {
updateAppId();
valueManager = createCellValueManager();
sheetOverlays = new HashSet();
tables = new HashSet();
registerRpc(new SpreadsheetHandlerImpl(this));
defaultActionHandler = new SpreadsheetDefaultActionHandler();
hyperlinkCellClickHandler = new DefaultHyperlinkCellClickHandler(this);
addActionHandler(defaultActionHandler);
setId(UUID.randomUUID().toString());
customInit();
}
private void updateAppId() {
Optional.ofNullable(UI.getCurrent()).ifPresent(ui -> {
getElement().setProperty("appId", ui.getInternals().getAppId());
});
}
private void registerRpc(SpreadsheetHandlerImpl spreadsheetHandler) {
addListener(SpreadsheetEvent.class,
new SpreadsheetEventListener(spreadsheetHandler));
}
/**
* Override if there are desired changes or temporary bug fixes, but be
* careful - this class should cache values for performance.
*
* @return CellValueManager
*/
protected CellValueManager createCellValueManager() {
return new CellValueManager(this);
}
/**
* Implement this to perform custom initialization in subclasses. Called
* before loading any workbook, at the end of the required init() actions.
*/
protected void customInit() {
// do nothing by default
}
/**
* Adds an action handler to the spreadsheet that handles the event produced
* by the context menu (right click) on cells and row and column headers.
* The action handler is component, not workbook, specific.
*
* The parameters on the
* {@link Action.Handler#handleAction(Action, Object, Object)} and
* {@link Action.Handler#getActions(Object, Object)} depend on the actual
* target of the right click.
*
* The second parameter (sender) on
* {@link Action.Handler#getActions(Object, Object)} is always the
* spreadsheet component. In case of a cell, the first parameter (target) on
* contains the latest {@link SelectionChangeEvent} for the spreadsheet. In
* case of a row or a column header, the first parameter (target) is a
* {@link CellRangeAddress}. To distinct between column / row header, you
* can use {@link CellRangeAddress#isFullColumnRange()} and
* {@link CellRangeAddress#isFullRowRange()}.
*
* Similarly for {@link Action.Handler#handleAction(Action, Object, Object)}
* the second parameter (sender) is always the spreadsheet component. The
* third parameter (target) is the latest {@link SelectionChangeEvent} for
* the spreadsheet, or the {@link CellRangeAddress} defining the selected
* row / column header.
*/
@Override
public void addActionHandler(Action.Handler actionHandler) {
contextMenuManager.addActionHandler(actionHandler);
setHasActions(contextMenuManager.hasActionHandlers());
}
/**
* Removes the spreadsheet's {@link SpreadsheetDefaultActionHandler} added
* on {@link Spreadsheet#init()}
*/
public void removeDefaultActionHandler() {
removeActionHandler(defaultActionHandler);
}
/*
* (non-Javadoc)
*
* @see
* com.vaadin.event.Action.Container#removeActionHandler(com.vaadin.event
* .Action.Handler)
*/
@Override
public void removeActionHandler(Action.Handler actionHandler) {
contextMenuManager.removeActionHandler(actionHandler);
setHasActions(contextMenuManager.hasActionHandlers());
}
/**
* Sets the {@link CellValueHandler} for this component (not workbook/sheet
* specific). It is called when a cell's value has been updated by the user
* by using the spreadsheet component's default editor (text input).
*
* @param customCellValueHandler
* New handler or null
if none should be used
*/
public void setCellValueHandler(CellValueHandler customCellValueHandler) {
getCellValueManager().setCustomCellValueHandler(customCellValueHandler);
}
/**
* See {@link CellValueHandler}.
*
* @return the current {@link CellValueHandler} for this component or
* null
if none has been set
*/
public CellValueHandler getCellValueHandler() {
return getCellValueManager().getCustomCellValueHandler();
}
/**
* Sets the {@link CellDeletionHandler} for this component (not
* workbook/sheet specific). It is called when a cell has been deleted by
* the user.
*
* @param customCellDeletionHandler
* New handler or null
if none should be used
*/
public void setCellDeletionHandler(
CellDeletionHandler customCellDeletionHandler) {
getCellValueManager()
.setCustomCellDeletionHandler(customCellDeletionHandler);
}
/**
* See {@link CellDeletionHandler}.
*
* @return the current {@link CellDeletionHandler} for this component or
* null
if none has been set
*/
public CellDeletionHandler getCellDeletionHandler() {
return getCellValueManager().getCustomCellDeletionHandler();
}
/**
* Sets the {@link HyperlinkCellClickHandler} for this component (not
* workbook/sheet specific). Called when the user clicks a cell that is a
* hyperlink or uses the hyperlink function.
*
* @param handler
* new handler or null
if none should be used
* @see HyperlinkCellClickHandler
* @see DefaultHyperlinkCellClickHandler
*/
public void setHyperlinkCellClickHandler(
HyperlinkCellClickHandler handler) {
hyperlinkCellClickHandler = handler;
}
/**
* See {@link HyperlinkCellClickHandler}.
*
* @return the current {@link HyperlinkCellClickHandler} for this component
* or null
if none has been set
*/
public HyperlinkCellClickHandler getHyperlinkCellClickHandler() {
return hyperlinkCellClickHandler;
}
/**
* Gets the ContextMenuManager for this Spreadsheet. This is component (not
* workbook/sheet) specific.
*
* @return The ContextMenuManager
*/
public ContextMenuManager getContextMenuManager() {
return contextMenuManager;
}
/**
* Gets the CellSelectionManager for this Spreadsheet. This is component
* (not workbook/sheet) specific.
*
* @return The CellSelectionManager
*/
public CellSelectionManager getCellSelectionManager() {
return selectionManager;
}
/**
* Gets the CellValueManager for this Spreadsheet. This is component (not
* workbook/sheet) specific.
*
* @return The CellValueManager
*/
public CellValueManager getCellValueManager() {
return valueManager;
}
/**
* Gets the CellShifter for this Spreadsheet. This is component (not
* workbook/sheet) specific.
*
* @return The CellShifter
*/
protected CellSelectionShifter getCellShifter() {
return cellShifter;
}
/**
* Gets the SpreadsheetHistoryManager for this Spreadsheet. This is
* component (not workbook/sheet) specific.
*
* @return The SpreadsheetHistoryManager
*/
public SpreadsheetHistoryManager getSpreadsheetHistoryManager() {
return historyManager;
}
/**
* Gets the MergedRegionContainer for this Spreadsheet. This is component
* (not workbook/sheet) specific.
*
* @return The MergedRegionContainer
*/
protected MergedRegionContainer getMergedRegionContainer() {
return mergedRegionContainer;
}
/**
* Returns the first visible column in the main scroll area (NOT freeze
* pane)
*
* @return Index of first visible column, 1-based
*/
public int getFirstColumn() {
return firstColumn;
}
/**
* Returns the last visible column in the main scroll area (NOT freeze pane)
*
* @return Index of last visible column, 1-based
*/
public int getLastColumn() {
return lastColumn;
}
/**
* Returns the first visible row in the scroll area (not freeze pane)
*
* @return Index of first visible row, 1-based
*/
public int getFirstRow() {
return firstRow;
}
/**
* Returns the last visible row in the main scroll area (NOT freeze pane)
*
* @return Index of last visible row, 1-based
*/
public int getLastRow() {
return lastRow;
}
/**
* Returns the index the last frozen row (last row in top freeze pane).
*
* @return Last frozen row or 0 if none
*/
public int getLastFrozenRow() {
return getVerticalSplitPosition();
}
/**
* Returns the index the last frozen column (last column in left freeze
* pane).
*
* @return Last frozen column or 0 if none
*/
public int getLastFrozenColumn() {
return getHorizontalSplitPosition();
}
/**
* Returns true if embedded charts are displayed
*
* @see #setChartsEnabled(boolean)
* @return
*/
public boolean isChartsEnabled() {
return chartsEnabled;
}
/**
* Use this method to define whether embedded charts should be displayed in
* the spreadsheet or not.
*
* @param chartsEnabled
*/
public void setChartsEnabled(boolean chartsEnabled) {
this.chartsEnabled = chartsEnabled;
clearSheetOverlays();
loadOrUpdateOverlays();
}
/**
* Returns true if the component is being fully re-rendered after this
* round-trip (sheet change etc.)
*
* @return true if re-render will happen, false otherwise
*/
public boolean isRerenderPending() {
return reload;
}
/*
* (non-Javadoc)
*
* @see
* com.vaadin.server.AbstractClientConnector#fireEvent(java.util.EventObject
* )
*/
@Override
protected void fireEvent(ComponentEvent event) {
super.fireEvent(event);
}
/**
* This method is called when the sheet is scrolled. It takes care of
* sending newly revealed data to the client side.
*
* @param firstRow
* Index of first visible row after the scroll, 1-based
* @param firstColumn
* Index of first visible column after the scroll, 1-based
* @param lastRow
* Index of last visible row after the scroll, 1-based
* @param lastColumn
* Index of first visible column after the scroll, 1-based
*/
protected void onSheetScroll(int firstRow, int firstColumn, int lastRow,
int lastColumn) {
if (reloadCellDataOnNextScroll || this.firstRow != firstRow
|| this.lastRow != lastRow || this.firstColumn != firstColumn
|| this.lastColumn != lastColumn) {
this.firstRow = firstRow;
this.lastRow = lastRow;
this.firstColumn = firstColumn;
this.lastColumn = lastColumn;
loadCells(firstRow, firstColumn, lastRow, lastColumn);
}
if (initialSheetSelection != null) {
selectionManager.onSheetAddressChanged(initialSheetSelection, true);
initialSheetSelection = null;
} else if (reloadCellDataOnNextScroll) {
selectionManager.reloadCurrentSelection();
}
reloadCellDataOnNextScroll = false;
}
/**
* Tells whether the given cell range is editable or not.
*
* @param cellRangeAddress
* Cell range to test
* @return True if range is editable, false otherwise.
*/
protected boolean isRangeEditable(CellRangeAddress cellRangeAddress) {
return isRangeEditable(cellRangeAddress.getFirstRow(),
cellRangeAddress.getFirstColumn(),
cellRangeAddress.getLastRow(),
cellRangeAddress.getLastColumn());
}
/**
* Determines if the given cell range is editable or not.
*
* @param row1
* Index of starting row, 0-based
* @param col1
* Index of starting column, 0-based
* @param row2
* Index of ending row, 0-based
* @param col2
* Index of ending column, 0-based
*
* @return True if the whole range is editable, false otherwise.
*/
protected boolean isRangeEditable(int row1, int col1, int row2, int col2) {
if (isActiveSheetProtected()) {
for (int r = row1; r <= row2; r++) {
final Row row = getActiveSheet().getRow(r);
if (row != null) {
for (int c = col1; c <= col2; c++) {
final Cell cell = row.getCell(c);
if (isCellLocked(cell)) {
return false;
}
}
} else {
return false;
}
}
}
return true;
}
/**
* Creates a CellRangeAddress from the given cell address string. Also
* checks that the range is valid within the currently active sheet. If it
* is not, the resulting range will be truncated to fit the active sheet.
*
* @param addressString
* Cell address string, e.g. "B3:C5"
* @return A CellRangeAddress based on the given coordinates.
*/
protected CellRangeAddress createCorrectCellRangeAddress(
String addressString) {
final String[] split = addressString.split(":");
final CellReference cr1 = new CellReference(split[0]);
final CellReference cr2 = new CellReference(split[1]);
int r1 = cr1.getRow() > cr2.getRow() ? cr2.getRow() : cr1.getRow();
int r2 = cr1.getRow() > cr2.getRow() ? cr1.getRow() : cr2.getRow();
int c1 = cr1.getCol() > cr2.getCol() ? cr2.getCol() : cr1.getCol();
int c2 = cr1.getCol() > cr2.getCol() ? cr1.getCol() : cr2.getCol();
if (r1 >= getRows()) {
r1 = getRows() - 1;
}
if (r2 >= getRows()) {
r2 = getRows() - 1;
}
if (c1 >= getCols()) {
c1 = getCols() - 1;
}
if (c2 >= getCols()) {
c2 = getCols() - 1;
}
return new CellRangeAddress(r1, r2, c1, c2);
}
/**
* Creates a CellRangeAddress from the given start and end coordinates. Also
* checks that the range is valid within the currently active sheet. If it
* is not, the resulting range will be truncated to fit the active sheet.
*
* @param row1
* Index of the starting row, 1-based
* @param col1
* Index of the starting column, 1-based
* @param row2
* Index of the ending row, 1-based
* @param col2
* Index of the ending column, 1-based
*
* @return A CellRangeAddress based on the given coordinates.
*/
protected CellRangeAddress createCorrectCellRangeAddress(int row1, int col1,
int row2, int col2) {
int r1 = row1 > row2 ? row2 : row1;
int r2 = row1 > row2 ? row1 : row2;
int c1 = col1 > col2 ? col2 : col1;
int c2 = col1 > col2 ? col1 : col2;
if (r1 >= getRows()) {
r1 = getRows();
}
if (r2 >= getRows()) {
r2 = getRows();
}
if (c1 >= getCols()) {
c1 = getCols();
}
if (c2 >= getCols()) {
c2 = getCols();
}
return new CellRangeAddress(r1 - 1, r2 - 1, c1 - 1, c2 - 1);
}
/**
* Set the Locale for the Spreadsheet. The locale is used for formatting
* cell values.
*
* @param locale
* the locale set to the spreadsheet, cannot be null
*/
public void setLocale(Locale locale) {
Objects.requireNonNull(locale, "Locale must not be null.");
this.locale = locale;
valueManager.updateLocale(locale);
refreshAllCellValues();
}
/**
* Gets the Locale for this spreadsheet
*
* @return the locale used for spreadsheet
*/
@Override
public Locale getLocale() {
if (locale != null) {
return locale;
} else {
return super.getLocale();
}
}
@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
valueManager.updateLocale(getLocale());
updateAppId();
if (overlays != null) {
// The node id's of component overlays attached as virtual children
// may no longer be valid after a detach/attach. Remove all
// overlays and reload them (with updated node id's).
overlays.clear();
loadOrUpdateOverlays();
}
if (componentIDtoCellKeysMap != null || cellKeysToEditorIdMap != null) {
// The node id's of custom components may no longer be valid after a
// detach/attach. Remove all custom components and reload them (with
// updated node id's).
loadCustomComponents();
}
}
/**
* See {@link Workbook#setSheetHidden(int, boolean)}.
*
* Gets the Workbook with {@link #getWorkbook()} and uses its API to access
* status on currently visible/hidden/very hidden sheets.
*
* If the currently active sheet is set hidden, another sheet is set as
* active sheet automatically. At least one sheet should be always visible.
*
* @param sheetPOIIndex
* Index of the target sheet within the POI model, 0-based
* @param visibility
* Visibility state to set: visible, hidden, very hidden.
* @throws IllegalArgumentException
* If the index or state is invalid, or if trying to hide the
* only visible sheet.
*/
public void setSheetHidden(int sheetPOIIndex, SheetVisibility visibility)
throws IllegalArgumentException {
// POI allows user to hide all sheets ...
if (visibility != SheetVisibility.VISIBLE
&& SpreadsheetUtil.getNumberOfVisibleSheets(workbook) == 1
&& !(workbook.isSheetHidden(sheetPOIIndex)
|| workbook.isSheetVeryHidden(sheetPOIIndex))) {
throw new IllegalArgumentException(
"At least one sheet should be always visible.");
}
boolean isHidden = workbook.isSheetHidden(sheetPOIIndex);
boolean isVeryHidden = workbook.isSheetVeryHidden(sheetPOIIndex);
int activeSheetIndex = workbook.getActiveSheetIndex();
workbook.setSheetVisibility(sheetPOIIndex, visibility);
// skip component reload if "nothing changed"
if ((visibility == SheetVisibility.VISIBLE
&& (isHidden || isVeryHidden))
|| (visibility != SheetVisibility.VISIBLE
&& !(isHidden || isVeryHidden))) {
if (sheetPOIIndex != activeSheetIndex) {
reloadSheetNames();
setSheetIndex(getSpreadsheetSheetIndex(activeSheetIndex) + 1);
} else { // the active sheet can be only set as hidden
int oldVisibleSheetIndex = getSheetIndex() - 1;
if (visibility != SheetVisibility.VISIBLE
&& activeSheetIndex == (workbook.getNumberOfSheets()
- 1)) {
// hiding the active sheet, and it was the last sheet
oldVisibleSheetIndex--;
}
int newActiveSheetIndex = getVisibleSheetPOIIndex(
oldVisibleSheetIndex);
workbook.setActiveSheet(newActiveSheetIndex);
reloadActiveSheetData();
SpreadsheetFactory.reloadSpreadsheetData(this,
getActiveSheet());
getSpreadsheetStyleFactory().reloadActiveSheetCellStyles();
}
}
}
/**
* See {@link Workbook#setSheetHidden(int, boolean)}.
*
* Gets the Workbook with {@link #getWorkbook()} and uses its API to access
* status on currently visible/hidden/very hidden sheets.
*
* If the currently active sheet is set hidden, another sheet is set as
* active sheet automatically. At least one sheet should be always visible.
*
* @param hidden
* Visibility state to set: 0-visible, 1-hidden, 2-very hidden.
* @param sheetPOIIndex
* Index of the target sheet within the POI model, 0-based
* @throws IllegalArgumentException
* If the index or state is invalid, or if trying to hide the
* only visible sheet.
* @deprecated use {@link #setSheetHidden(int, SheetVisibility)}
*/
@Deprecated
public void setSheetHidden(int sheetPOIIndex, int hidden)
throws IllegalArgumentException {
setSheetHidden(sheetPOIIndex, SheetVisibility.values()[hidden]);
}
/**
* Returns an array containing the names of the currently visible sheets.
* Does not contain the names of hidden or very hidden sheets.
*
* To get all of the current {@link Workbook}'s sheet names, you should
* access the POI API with {@link #getWorkbook()}.
*
* @return Names of the currently visible sheets.
*/
public String[] getVisibleSheetNames() {
final String[] names = getSheetNames();
return Arrays.copyOf(names, names.length);
}
/**
* Sets a name for the sheet at the given visible sheet index.
*
* @param sheetIndex
* Index of the target sheet among the visible sheets, 0-based
* @param sheetName
* New sheet name. Not null, empty nor longer than 31 characters.
* Must be unique within the Workbook.
* @throws IllegalArgumentException
* If the index is invalid, or if the sheet name is invalid. See
* {@link WorkbookUtil#validateSheetName(String)}.
*/
public void setSheetName(int sheetIndex, String sheetName)
throws IllegalArgumentException {
if (sheetIndex < 0 || sheetIndex >= getSheetNames().length) {
throw new IllegalArgumentException("Invalid Sheet index given.");
}
int poiSheetIndex = getVisibleSheetPOIIndex(sheetIndex);
setSheetNameWithPOIIndex(poiSheetIndex, sheetName);
}
/**
* Sets a name for the sheet at the given POI model index.
*
* @param sheetIndex
* Index of the target sheet within the POI model, 0-based
* @param sheetName
* New sheet name. Not null, empty nor longer than 31 characters.
* Must be unique within the Workbook.
* @throws IllegalArgumentException
* If the index is invalid, or if the sheet name is invalid. See
* {@link WorkbookUtil#validateSheetName(String)}.
*
*/
public void setSheetNameWithPOIIndex(int sheetIndex, String sheetName)
throws IllegalArgumentException {
if (sheetIndex < 0 || sheetIndex >= workbook.getNumberOfSheets()) {
throw new IllegalArgumentException(
"Invalid POI Sheet index given.");
}
if (sheetName == null || sheetName.isEmpty()) {
throw new IllegalArgumentException(
"Sheet Name cannot be null or an empty String, or contain backslash \\.");
}
if (isSheetNameExisting(sheetName)) {
throw new IllegalArgumentException(
"Sheet name must be unique within the workbook.");
}
workbook.setSheetName(sheetIndex, sheetName);
if (!workbook.isSheetVeryHidden(sheetIndex)
&& !workbook.isSheetHidden(sheetIndex)) {
int ourIndex = getSpreadsheetSheetIndex(sheetIndex);
String[] _sheetNames = Arrays.copyOf(getSheetNames(),
getSheetNames().length);
_sheetNames[ourIndex] = sheetName;
setSheetNames(_sheetNames);
}
}
/**
* Sets the protection enabled with the given password for the sheet at the
* given index. null
password removes the protection.
*
* @param sheetPOIIndex
* Index of the target sheet within the POI model, 0-based
* @param password
* The password to set for the protection. Pass null
* to remove the protection.
*/
public void setSheetProtected(int sheetPOIIndex, String password) {
if (sheetPOIIndex < 0
|| sheetPOIIndex >= workbook.getNumberOfSheets()) {
throw new IllegalArgumentException(
"Invalid POI Sheet index given.");
}
workbook.getSheetAt(sheetPOIIndex).protectSheet(password);
setSheetProtected(getActiveSheet().getProtect());
// if the currently active sheet was protected, the protection for the
// currently selected cell might have changed
if (sheetPOIIndex == workbook.getActiveSheetIndex()) {
loadCustomComponents();
selectionManager.reSelectSelectedCell();
}
}
/**
* Sets the protection enabled with the given password for the currently
* active sheet. null
password removes the protection.
*
* @param password
* The password to set for the protection. Pass null
* to remove the protection.
*/
public void setActiveSheetProtected(String password) {
setSheetProtected(workbook.getActiveSheetIndex(), password);
}
/**
* Creates a new sheet as the last sheet and sets it as the active sheet.
*
* If the sheetName given is null, then the sheet name is automatically
* generated by Apache POI in {@link Workbook#createSheet()}.
*
* @param sheetName
* Can be null, but not empty nor longer than 31 characters. Must
* be unique within the Workbook.
* @param rows
* Number of rows the sheet should have
* @param columns
* Number of columns the sheet should have
* @throws IllegalArgumentException
* If the sheet name is empty or over 31 characters long or not
* unique.
*/
public void createNewSheet(String sheetName, int rows, int columns)
throws IllegalArgumentException {
if (sheetName != null && sheetName.isEmpty()) {
throw new IllegalArgumentException(
"Sheet Name cannot be an empty String.");
}
if (sheetName != null && sheetName.length() > 31) {
throw new IllegalArgumentException(
"Sheet Name cannot be longer than 31 characters");
}
if (sheetName != null && isSheetNameExisting(sheetName)) {
throw new IllegalArgumentException(
"Sheet name must be unique within the workbook.");
}
final Sheet previousSheet = getActiveSheet();
SpreadsheetFactory.addNewSheet(this, workbook, sheetName, rows,
columns);
fireSheetChangeEvent(previousSheet, getActiveSheet());
}
/**
* Deletes the sheet with the given POI model index.
*
* Note: A workbook must contain at least one visible sheet.
*
* @param poiSheetIndex
* POI model index of the sheet to delete, 0-based, max value
* {@link Workbook#getNumberOfSheets()} -1.
* @throws IllegalArgumentException
* In case there is only one visible sheet, or if the index is
* invalid.
*/
public void deleteSheetWithPOIIndex(int poiSheetIndex)
throws IllegalArgumentException {
if (getNumberOfVisibleSheets() < 2) {
throw new IllegalArgumentException(
"A workbook must contain at least one visible worksheet");
}
int removedVisibleIndex = getSpreadsheetSheetIndex(poiSheetIndex);
workbook.removeSheetAt(poiSheetIndex);
// POI doesn't seem to shift the active sheet index ...
// TODO: This no longer seems to be the case. Remove the following
// logic?
int oldIndex = getSheetIndex() - 1;
if (removedVisibleIndex <= oldIndex) { // removed before current
if (oldIndex == (getNumberOfVisibleSheets())) {
// need to shift index backwards if the current sheet is last
workbook.setActiveSheet(getVisibleSheetPOIIndex(oldIndex - 1));
} else {
workbook.setActiveSheet(getVisibleSheetPOIIndex(oldIndex));
}
}
// need to reload everything because there is a ALWAYS chance that the
// removed sheet effects the currently visible sheet (via cell formulas
// etc.)
reloadActiveSheetData();
}
/**
* Deletes the sheet at the given index.
*
* Note: A workbook must contain at least one visible sheet.
*
* @param sheetIndex
* Index of the sheet to delete among the visible sheets,
* 0-based, maximum value {@link #getNumberOfVisibleSheets()} -1.
* @throws IllegalArgumentException
* In case there is only one visible sheet, or if the given
* index is invalid.
*/
public void deleteSheet(int sheetIndex) throws IllegalArgumentException {
if (sheetIndex < 0 || sheetIndex >= getNumberOfVisibleSheets()) {
throw new IllegalArgumentException(
"Invalid index for visible sheet given.");
}
deleteSheetWithPOIIndex(getVisibleSheetPOIIndex(sheetIndex));
}
/**
* Returns the number of currently visible sheets in the component. Doesn't
* include the hidden or very hidden sheets in the POI model.
*
* @return Number of visible sheets.
*/
public int getNumberOfVisibleSheets() {
if (getSheetNames() != null) {
return getSheetNames().length;
} else {
return 0;
}
}
/**
* Returns the total number of sheets in the workbook (includes hidden and
* very hidden sheets).
*
* @return Total number of sheets in the workbook
*/
public int getNumberOfSheets() {
return workbook.getNumberOfSheets();
}
private boolean isSheetNameExisting(String sheetName) {
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
if (workbook.getSheetName(i).equals(sheetName)) {
return true;
}
}
return false;
}
/**
* Returns the index of the currently active sheet among the visible sheets
* ( hidden or very hidden sheets not included).
*
* @return Index of the active sheet, 0-based
*/
public int getActiveSheetIndex() {
return getSheetIndex() - 1;
}
/**
* Returns the POI model index of the currently active sheet (index among
* all sheets including hidden and very hidden sheets).
*
* @return POI model index of the active sheet, 0-based
*/
public int getActiveSheetPOIIndex() {
return getVisibleSheetPOIIndex(getSheetIndex() - 1);
}
/**
* Sets the currently active sheet within the sheets that are visible.
*
* @param sheetIndex
* Index of the target sheet (among the visible sheets), 0-based
* @throws IllegalArgumentException
* If the index is invalid
*/
public void setActiveSheetIndex(int sheetIndex)
throws IllegalArgumentException {
if (sheetIndex < 0 || sheetIndex >= getSheetNames().length) {
throw new IllegalArgumentException("Invalid Sheet index given.");
}
int POISheetIndex = getVisibleSheetPOIIndex(sheetIndex);
setActiveSheetWithPOIIndex(POISheetIndex);
}
/**
* Sets the currently active sheet. The sheet at the given index should be
* visible (not hidden or very hidden).
*
* @param sheetIndex
* Index of sheet in the POI model (contains all sheets), 0-based
* @throws IllegalArgumentException
* If the index is invalid, or if the sheet at the given index
* is hidden or very hidden.
*/
public void setActiveSheetWithPOIIndex(int sheetIndex)
throws IllegalArgumentException {
if (sheetIndex < 0 || sheetIndex >= workbook.getNumberOfSheets()) {
throw new IllegalArgumentException(
"Invalid POI Sheet index given.");
}
if (workbook.isSheetHidden(sheetIndex)
|| workbook.isSheetVeryHidden(sheetIndex)) {
throw new IllegalArgumentException(
"Cannot set a hidden or very hidden sheet as the active sheet. Given index: "
+ sheetIndex);
}
workbook.setActiveSheet(sheetIndex);
// assume since the UI doesn't allow multiple sheet selections
// active sheet == selected tab
workbook.setSelectedTab(sheetIndex);
// formulas defined relative to the sheet may need recalculation
getFormulaEvaluator().clearAllCachedResultValues();
getConditionalFormattingEvaluator().clearAllCachedValues();
reloadActiveSheetData();
SpreadsheetFactory.reloadSpreadsheetData(this,
workbook.getSheetAt(sheetIndex));
reloadActiveSheetStyles();
loadPopupButtons();
}
/**
* This method will be called when a selected sheet change is requested.
*
* @param tabIndex
* Index of the sheet to select.
* @param scrollLeft
* Current horizontal scroll position
* @param scrollTop
* Current vertical scroll position
*/
protected void onSheetSelected(int tabIndex, int scrollLeft,
int scrollTop) {
// this is for the very rare occasion when the sheet has been
// selected and the selected sheet value is still negative
int oldIndex = Math.abs(getSheetIndex()) - 1;
int[] _verticalScrollPositions = Arrays.copyOf(
getVerticalScrollPositions(),
getVerticalScrollPositions().length);
_verticalScrollPositions[oldIndex] = scrollTop;
setVerticalScrollPositions(_verticalScrollPositions);
int[] _horizontalScrollPositions = Arrays.copyOf(
getHorizontalScrollPositions(),
getHorizontalScrollPositions().length);
_horizontalScrollPositions[oldIndex] = scrollLeft;
setHorizontalScrollPositions(_horizontalScrollPositions);
Sheet oldSheet = getActiveSheet();
setActiveSheetIndex(tabIndex);
Sheet newSheet = getActiveSheet();
fireSheetChangeEvent(oldSheet, newSheet);
}
/**
* This method is called when the creation of a new sheet has been
* requested.
*
* @param scrollLeft
* Current horizontal scroll position
* @param scrollTop
* Current vertical scroll position
*/
protected void onNewSheetCreated(int scrollLeft, int scrollTop) {
int[] _verticalScrollPositions = Arrays.copyOf(
getVerticalScrollPositions(),
getVerticalScrollPositions().length);
_verticalScrollPositions[getSheetIndex() - 1] = scrollTop;
setVerticalScrollPositions(_verticalScrollPositions);
int[] _horizontalScrollPositions = Arrays.copyOf(
getHorizontalScrollPositions(),
getHorizontalScrollPositions().length);
_horizontalScrollPositions[getSheetIndex() - 1] = scrollLeft;
setHorizontalScrollPositions(_horizontalScrollPositions);
createNewSheet(null, defaultNewSheetRows, defaultNewSheetColumns);
}
/**
* This method is called when a request to rename a sheet has been made.
*
* @param sheetIndex
* Index of the sheet to rename (among visible sheets).
* @param sheetName
* New name for the sheet.
*/
protected void onSheetRename(int sheetIndex, String sheetName) {
// if excel doesn't keep these in history, neither will we
setSheetNameWithPOIIndex(getVisibleSheetPOIIndex(sheetIndex),
sheetName);
}
/**
* Get the number of columns in the currently active sheet, or if
* {@link #setMaxColumns(int)} has been used, the current number of columns
* the component shows (not the amount of columns in the actual sheet in the
* POI model).
*
* @return Number of visible columns.
*/
public int getColumns() {
return getCols();
}
/**
* Get the number of rows in the currently active sheet, or if
* {@link #setMaxRows(int)} has been used, the current number of rows the
* component shows (not the amount of rows in the actual sheet in the POI
* model).
*
* @return Number of visible rows.
*/
public int getRows() {
return rows;
}
/**
* Gets the current DataFormatter.
*
* @return The data formatter for this Spreadsheet.
*/
public DataFormatter getDataFormatter() {
return valueManager.getDataFormatter();
}
/**
* Returns the Cell at the given address. If the cell is updated in outside
* code, call {@link #refreshCells(Cell...)} AFTER ALL UPDATES (value, type,
* formatting or style) to mark the cell as "dirty".
*
* @param cellAddress
* Address of the Cell to return, e.g. "A3"
* @return The cell at the given address, or null if not defined
*/
public Cell getCell(String cellAddress) {
CellReference ref = new CellReference(cellAddress);
Row r = workbook.getSheetAt(workbook.getActiveSheetIndex())
.getRow(ref.getRow());
if (r != null) {
return r.getCell(ref.getCol());
} else {
return null;
}
}
/**
* Returns the Cell at the given coordinates. If the cell is updated in
* outside code, call {@link #refreshCells(Cell...)} AFTER ALL UPDATES
* (value, type, formatting or style) to mark the cell as "dirty".
*
* @param row
* Row index of the cell to return, 0-based
* @param col
* Column index of the cell to return, 0-based
* @return The cell at the given coordinates, or null if not defined
*/
public Cell getCell(int row, int col) {
Sheet sheet = workbook.getSheetAt(workbook.getActiveSheetIndex());
return getCell(row, col, sheet);
}
/**
* Returns the Cell at the given coordinates. If the cell is updated in
* outside code, call {@link #refreshCells(Cell...)} AFTER ALL UPDATES
* (value, type, formatting or style) to mark the cell as "dirty".
*
* @param row
* Row index of the cell to return, 0-based
* @param col
* Column index of the cell to return, 0-based
* @param sheet
* Sheet of the cell
* @return The cell at the given coordinates, or null if not defined
*/
public Cell getCell(int row, int col, Sheet sheet) {
Row r = sheet.getRow(row);
if (r != null) {
return r.getCell(col);
} else {
return null;
}
}
/**
* Returns the Cell corresponding to the given reference. If the cell is
* updated in outside code, call {@link #refreshCells(Cell...)} AFTER ALL
* UPDATES (value, type, formatting or style) to mark the cell as "dirty".
*
* @param cellReference
* Reference to the cell to return
* @return The cell corresponding to the given reference, or null if not
* defined
*/
public Cell getCell(CellReference cellReference) {
return cellReference == null ? null
: getCell(cellReference.getSheetName(), cellReference.getRow(),
cellReference.getCol());
}
/**
* Returns the Cell at the given coordinates. If the cell is updated in
* outside code, call {@link #refreshCells(Cell...)} AFTER ALL UPDATES
* (value, type, formatting or style) to mark the cell as "dirty".
*
* @param sheetName
* Name of the sheet the cell is on, or current sheet if null
* @param row
* Row index of the cell to return, 0-based
* @param column
* Column index of the cell to return, 0-based
* @return The cell at the given coordinates, or null if not defined
*/
public Cell getCell(String sheetName, int row, int column) {
if (sheetName == null)
return getCell(row, column);
return getCell(row, column, workbook.getSheet(sheetName));
}
/**
* Returns the Cell corresponding to the given reference. If the cell is
* updated in outside code, call {@link #refreshCells(Cell...)} AFTER ALL
* UPDATES (value, type, formatting or style) to mark the cell as "dirty".
*
* @param cellReference
* Reference to the cell to return
* @param sheet
* Sheet of the cell
* @return The cell corresponding to the given reference, or null if not
* defined
*/
public Cell getCell(CellReference cellReference, Sheet sheet) {
return cellReference == null ? null
: getCell(cellReference.getRow(), cellReference.getCol(),
sheet);
}
/**
* Deletes the cell from the sheet and the underlying POI model as well.
* This really deletes the cell, instead of just making it's value blank.
*
* @param row
* Row index of the cell to delete, 0-based
* @param col
* Column index of the cell to delete, 0-based
*/
public void deleteCell(int row, int col) {
final Sheet activeSheet = workbook
.getSheetAt(workbook.getActiveSheetIndex());
final Cell cell = activeSheet.getRow(row).getCell(col);
if (cell != null) {
// cell.setCellStyle(null); // TODO NPE on HSSF
styler.cellStyleUpdated(cell, true);
activeSheet.getRow(row).removeCell(cell);
valueManager.cellDeleted(cell);
refreshCells(cell);
}
}
/**
* Refreshes the given cell(s). Should be called when the cell
* value/formatting/style/etc. updating is done.
*
* NOTE: For optimal performance temporarily collect your updated cells and
* call this method only once per update cycle. Calling this method
* repeatedly for individual cells is not a good idea.
*
* @param cells
* Cell(s) to update
*/
public void refreshCells(Cell... cells) {
if (cells != null) {
for (Cell cell : cells) {
markCellAsUpdated(cell, true);
}
updateMarkedCells();
}
}
/**
* Refreshes the given cell(s). Should be called when the cell
* value/formatting/style/etc. updating is done.
*
* NOTE: For optimal performance temporarily collect your updated cells and
* call this method only once per update cycle. Calling this method
* repeatedly for individual cells is not a good idea.
*
* @param cells
* A Collection of Cells to update
*/
public void refreshCells(Collection cells) {
if (cells != null && !cells.isEmpty()) {
for (Cell cell : cells) {
markCellAsUpdated(cell, true);
}
updateMarkedCells();
}
}
/**
* Marks the cell as updated. Should be called when the cell
* value/formatting/style/etc. updating is done.
*
* @param cellStyleUpdated
* True if the cell style has changed
*
* @param cell
* The updated cell
*/
void markCellAsUpdated(Cell cell, boolean cellStyleUpdated) {
valueManager.cellUpdated(cell);
if (cellStyleUpdated) {
styler.cellStyleUpdated(cell, true);
}
}
/**
* Marks the cell as deleted. This method should be called after removing a
* cell from the {@link Workbook} using POI API.
*
* @param cellStyleUpdated
* True if the cell style has changed
* @param cell
* The cell that has been deleted.
*/
public void markCellAsDeleted(Cell cell, boolean cellStyleUpdated) {
valueManager.cellDeleted(cell);
if (cellStyleUpdated) {
styler.cellStyleUpdated(cell, true);
}
refreshCells(cell);
}
/**
* Updates the content of the cells that have been marked for update with
* {@link #markCellAsUpdated(Cell, boolean)}.
*
* Does NOT update custom components (editors / always visible) for the
* cells. For that, use {@link #reloadVisibleCellContents()}
*/
void updateMarkedCells() {
// update conditional formatting in case styling has changed. New values
// are fetched in ValueManager (below).
conditionalFormatter.createConditionalFormatterRules();
// FIXME should be optimized, should not go through all links, comments
// etc. always
valueManager.updateMarkedCellValues();
// if the selected cell is of type formula, there is a change that the
// formula has been changed.
selectionManager.reSelectSelectedCell();
// Update the cell comments as well to show them instantly after adding
// them
loadCellComments();
// update custom components, editors
reloadVisibleCellContents();
}
/**
* Creates a new Formula type cell with the given formula.
*
* After all editing is done, call {@link #refreshCells(Cell...)} or
* {@link #refreshAllCellValues()} to make sure client side is updated.
*
* @param row
* Row index of the new cell, 0-based
* @param col
* Column index of the new cell, 0-based
* @param formula
* The formula to set to the new cell (should NOT start with "="
* nor "+")
* @return The newly created cell
* @throws IllegalArgumentException
* If columnIndex < 0 or greater than the maximum number of
* supported columns (255 for *.xls, 1048576 for *.xlsx)
*/
public Cell createFormulaCell(int row, int col, String formula)
throws IllegalArgumentException {
final Sheet activeSheet = workbook
.getSheetAt(workbook.getActiveSheetIndex());
Row r = activeSheet.getRow(row);
if (r == null) {
r = activeSheet.createRow(row);
}
Cell cell = r.getCell(col);
if (cell == null) {
cell = r.createCell(col, CellType.FORMULA);
} else {
final String key = SpreadsheetUtil.toKey(col + 1, row + 1);
valueManager.clearCellCache(key);
}
cell.setCellFormula(formula);
valueManager.cellUpdated(cell);
return cell;
}
/**
* Create a new cell (or replace existing) with the given value, the type of
* the value parameter will define the type of the cell. The value may be of
* the following types: Boolean, Calendar, Date, Double or String. The
* default type will be String, value of ({@link #toString()} will be given
* as the cell value.
*
* For formula cells, use {@link #createFormulaCell(int, int, String)}.
*
* After all editing is done, call {@link #refreshCells(Cell...)} or
* {@link #refreshAllCellValues()} to make sure the client side is updated.
*
* @param row
* Row index of the new cell, 0-based
* @param col
* Column index of the new cell, 0-based
* @param value
* Object representing the type and value of the Cell
* @return The newly created cell
* @throws IllegalArgumentException
* If columnIndex < 0 or greater than the maximum number of
* supported columns (255 for *.xls, 1048576 for *.xlsx)
*/
public Cell createCell(int row, int col, Object value)
throws IllegalArgumentException {
final Sheet activeSheet = workbook
.getSheetAt(workbook.getActiveSheetIndex());
Row r = activeSheet.getRow(row);
if (r == null) {
r = activeSheet.createRow(row);
}
Cell cell = r.getCell(col);
if (cell == null) {
cell = r.createCell(col);
} else {
final String key = SpreadsheetUtil.toKey(col + 1, row + 1);
valueManager.clearCellCache(key);
}
if (value instanceof Double) {
cell.setCellValue((Double) value);
} else if (value instanceof Boolean) {
cell.setCellValue((Boolean) value);
} else if (value instanceof Date) {
cell.setCellValue((Date) value);
} else if (value instanceof Calendar) {
cell.setCellValue((Calendar) value);
} else if (value != null) {
cell.setCellValue(value.toString());
}
valueManager.cellUpdated(cell);
// if programmatically adding cells, need to make sure they display
if (row > getRows()) {
setMaxRows(row);
}
return cell;
}
/**
* Forces recalculation and update to the client side for values of all of
* the sheet's cells.
*
* Note: DOES NOT UPDATE STYLES; use {@link #refreshCells(Cell...)} when
* cell styles change.
*/
public void refreshAllCellValues() {
getFormulaEvaluator().clearAllCachedResultValues();
getConditionalFormattingEvaluator().clearAllCachedValues();
valueManager.clearCachedContent();
// only reload if the cells have been loaded once previously
if (firstColumn == -1) {
// client will request cells soon, no need for reload now
return;
}
updateRowAndColumnRangeCellData(1, 1, getRows(), getColumns());
// if the selected cell is of type formula, there is a change that the
// formula has been changed.
selectionManager.reSelectSelectedCell();
}
/**
* Set the number of columns shown for the current sheet. Any null cells are
* left empty. Any cells outside the given columns are hidden. Does not
* update the actual POI-based model!
*
* The default value will be the actual size of the sheet from the POI
* model.
*
* @param cols
* New maximum column count.
*/
public void setMaxColumns(int cols) {
if (getCols() != cols) {
setCols(cols);
}
}
/**
* Set the number of rows shown for the current sheet. Any null cells are
* left empty. Any cells outside the given rows are hidden. Does not update
* the actual POI-based model!
*
* The default value will be the actual size of the sheet from the POI
* model.
*
* @param rows
* New maximum row count.
*/
public void setMaxRows(int rows) {
if (getRows() != rows) {
setRows(rows);
}
}
/**
* Does {@link #setMaxColumns(int)} and {@link #setMaxRows(int)} in one
* method.
*
* @param rows
* Maximum row count
* @param cols
* Maximum column count
*/
public void setSheetMaxSize(int rows, int cols) {
setCols(cols);
setRows(rows);
}
/**
* Gets the default column width for the currently active sheet. This is
* derived from the active sheet's ({@link #getActiveSheet()}) default
* column width (Sheet {@link #getDefaultColumnWidth()}).
*
* @return The default column width in PX
*/
public int getDefaultColumnWidth() {
return getDefColW();
}
/**
* Sets the default column width in pixels that the component uses, this
* doesn't change the default column width of the underlying sheet, returned
* by {@link #getActiveSheet()} and {@link Sheet#getDefaultColumnWidth()}.
*
* @param widthPX
* The default column width in pixels
*/
public void setDefaultColumnWidth(int widthPX) {
if (widthPX <= 0) {
throw new IllegalArgumentException(
"Default column width must be over 0, given value: "
+ widthPX);
}
setDefColW(widthPX);
}
/**
* Gets the default row height in points. By default it should be the same
* as {@link Sheet#getDefaultRowHeightInPoints()} for the currently active
* sheet {@link #getActiveSheet()}.
*
* @return Default row height for the currently active sheet, in points.
*/
public float getDefaultRowHeight() {
return getDefRowH();
}
/**
* Sets the default row height in points for this Spreadsheet and the
* currently active sheet, returned by {@link #getActiveSheet()}.
*
* @param heightPT
* New default row height in points.
*/
public void setDefaultRowHeight(float heightPT) {
if (heightPT <= 0.0f) {
throw new IllegalArgumentException(
"Default row height must be over 0, given value: "
+ heightPT);
}
getActiveSheet().setDefaultRowHeightInPoints(heightPT);
setDefRowH(heightPT);
}
/**
* This method is called when column auto-fit has been initiated from the
* browser by double-clicking the border of the target column header.
*
* @param columnIndex
* Index of the target column, 0-based
*/
protected void onColumnAutofit(int columnIndex) {
SizeChangeCommand command = new SizeChangeCommand(this, Type.COLUMN);
command.captureValues(new Integer[] { columnIndex + 1 });
autofitColumn(columnIndex);
historyManager.addCommand(command);
}
/**
* Sets the column to automatically adjust the column width to fit the
* largest cell content within the column. This is a POI feature, and is
* meant to be called after all the data for the target column has been
* written. See {@link Sheet#autoSizeColumn(int)}.
*
* This does not take into account cells that have custom Vaadin components
* inside them.
*
* @param columnIndex
* Index of the target column, 0-based
*/
public void autofitColumn(int columnIndex) {
final Sheet activeSheet = getActiveSheet();
try {
activeSheet.autoSizeColumn(columnIndex);
} catch (NullPointerException e) {
// NullPointerException is being thrown in POI. Catch to prevent
// breaking the UI.
LOGGER.trace(
"Poi threw NullPointerException when trying to autofit column",
e);
return;
}
int columnPixelWidth = getColumnAutofitPixelWidth(columnIndex,
(int) activeSheet.getColumnWidthInPixels(columnIndex));
int[] _colW = Arrays.copyOf(getColW(), getColW().length);
_colW[columnIndex] = columnPixelWidth;
setColW(_colW);
getCellValueManager().clearCacheForColumn(columnIndex + 1);
getCellValueManager().loadCellData(firstRow, columnIndex + 1, lastRow,
columnIndex + 1);
if (hasSheetOverlays()) {
reloadImageSizesFromPOI = true;
loadOrUpdateOverlays();
}
}
/**
* Shifts rows between startRow and endRow n number of rows. If you use a
* negative number for n, the rows will be shifted upwards. This method
* ensures that rows can't wrap around.
*
* If you are adding / deleting rows, you might want to change the number of
* visible rows rendered {@link #getRows()} with {@link #setMaxRows(int)}.
*
* See {@link Sheet#shiftRows(int, int, int)}.
*
* @param startRow
* The first row to shift, 0-based
* @param endRow
* The last row to shift, 0-based
* @param n
* Number of rows to shift, positive numbers shift down, negative
* numbers shift up.
*/
public void shiftRows(int startRow, int endRow, int n) {
shiftRows(startRow, endRow, n, false, false);
}
/**
* Shifts rows between startRow and endRow n number of rows. If you use a
* negative number for n, the rows will be shifted upwards. This method
* ensures that rows can't wrap around.
*
* If you are adding / deleting rows, you might want to change the number of
* visible rows rendered {@link #getRows()} with {@link #setMaxRows(int)}.
*
* See {@link Sheet#shiftRows(int, int, int, boolean, boolean)}.
*
* @param startRow
* The first row to shift, 0-based
* @param endRow
* The last row to shift, 0-based
* @param n
* Number of rows to shift, positive numbers shift down, negative
* numbers shift up.
* @param copyRowHeight
* True to copy the row height during the shift
* @param resetOriginalRowHeight
* True to set the original row's height to the default
*/
public void shiftRows(int startRow, int endRow, int n,
boolean copyRowHeight, boolean resetOriginalRowHeight) {
Sheet sheet = getActiveSheet();
int lastNonBlankRow = getLastNonBlankRow(sheet);
sheet.shiftRows(startRow, endRow, n, copyRowHeight,
resetOriginalRowHeight);
// need to re-send the cell values to client
// remove all cached cell data that is now empty
getFormulaEvaluator().clearAllCachedResultValues();
getConditionalFormattingEvaluator().clearAllCachedValues();
int start = n < 0 ? Math.max(lastNonBlankRow, startRow) : startRow;
int end = n < 0 ? endRow : startRow + n - 1;
valueManager.updateDeletedRowsInClientCache(start + 1, end + 1);
int firstAffectedRow = n < 0 ? startRow + n : startRow;
int lastAffectedRow = n < 0 ? endRow : endRow + n;
if (copyRowHeight || resetOriginalRowHeight) {
// might need to increase the size of the row heights array
int oldLength = getRowH().length;
int neededLength = endRow + n + 1;
float[] _rowH = Arrays.copyOf(getRowH(), getRowH().length);
if (n > 0 && oldLength < neededLength) {
_rowH = Arrays.copyOf(_rowH, neededLength);
}
for (int i = firstAffectedRow; i <= lastAffectedRow; i++) {
Row row = sheet.getRow(i);
if (row != null) {
if (row.getZeroHeight()) {
_rowH[i] = 0f;
} else {
_rowH[i] = row.getHeightInPoints();
}
} else {
_rowH[i] = sheet.getDefaultRowHeightInPoints();
}
}
setRowH(_rowH);
}
if (hasSheetOverlays()) {
reloadImageSizesFromPOI = true;
}
// need to shift the cell styles, clear and update
// need to go -1 and +1 because of shifted borders..
final ArrayList cellsToUpdate = new ArrayList();
for (int r = (firstAffectedRow - 1); r <= (lastAffectedRow + 1); r++) {
if (r < 0) {
r = 0;
}
Row row = sheet.getRow(r);
final Integer rowIndex = new Integer(r + 1);
ArrayList _hiddenRowIndexes = new ArrayList<>(
getHiddenRowIndexes());
if (row == null) {
valueManager.updateDeletedRowsInClientCache(rowIndex, rowIndex);
if (_hiddenRowIndexes.contains(rowIndex)) {
_hiddenRowIndexes.remove(rowIndex);
}
for (int c = 0; c < getCols(); c++) {
styler.clearCellStyle(r, c);
}
} else {
if (row.getZeroHeight()) {
_hiddenRowIndexes.add(rowIndex);
} else if (_hiddenRowIndexes.contains(rowIndex)) {
_hiddenRowIndexes.remove(rowIndex);
}
for (int c = 0; c < getCols(); c++) {
Cell cell = row.getCell(c);
if (cell == null) {
styler.clearCellStyle(r, c);
if (r <= lastNonBlankRow + n) {
// There might be a pre-shift value for this cell in
// client-side and should be overwritten
cell = row.createCell(c);
cellsToUpdate.add(cell);
}
} else {
cellsToUpdate.add(cell);
}
}
}
setHiddenRowIndexes(_hiddenRowIndexes);
}
rowsMoved(firstAffectedRow, lastAffectedRow, n);
for (Cell cell : cellsToUpdate) {
styler.cellStyleUpdated(cell, false);
markCellAsUpdated(cell, false);
}
styler.loadCustomBorderStylesToState();
updateMarkedCells(); // deleted and formula cells and style selectors
updateRowAndColumnRangeCellData(firstRow, firstColumn, lastRow,
lastColumn); // shifted area values
updateMergedRegions();
CellReference selectedCellReference = selectionManager
.getSelectedCellReference();
if (selectedCellReference != null) {
if (selectedCellReference.getRow() >= firstAffectedRow
&& selectedCellReference.getRow() <= lastAffectedRow) {
selectionManager.onSheetAddressChanged(
selectedCellReference.formatAsString(), false);
}
}
}
private boolean hasSheetOverlays() {
return sheetOverlays != null && sheetOverlays.size() > 0;
}
/**
* Checks if the current column has a filter popup button and calculates
* extra width to accommodate when to include it in autofit.
*
* @param columnIndex
* Index of the target column, 0 based
* @param autofitWidth
* The autofit width without the button, in pixels
* @return Pixel width of the column
*/
private int getColumnAutofitPixelWidth(int columnIndex, int autofitWidth) {
List tablesForActiveSheet = getTablesForActiveSheet();
CellReference cr = new CellReference(getActiveSheet().getSheetName(), 0,
columnIndex, true, true);
autofittedColumnWidths.put(cr, autofitWidth);
for (SpreadsheetTable st : tablesForActiveSheet) {
if (!(st instanceof SpreadsheetFilterTable)) {
continue;
}
SpreadsheetFilterTable ft = (SpreadsheetFilterTable) st;
PopupButton popupButton = ft.getPopupButton(cr);
if (popupButton != null) {
return autofitWidth + FILTER_BUTTON_PIXEL_WIDTH
+ FILTER_BUTTON_PIXEL_PADDING;
}
}
return autofitWidth;
}
/**
* Called when number of rows has moved. Spreadsheet needs to update its
* internal state.
*
* Note: If n is negative it would mean the rows has moved up. Positive
* value indicates that new rows are moved below.
*
* @param first
* the first row that has changed, 0-based
* @param last
* the last row that has changed, 0-based
* @param n
* the amount of lines that rows has been moved
*/
private void rowsMoved(int first, int last, int n) {
// Merged regions
if (n < 0) {
// Remove merged cells from deleted rows. POI will handle the other
// updated values.
for (int row = (first + n); row <= first; ++row) {
Sheet sheet = getActiveSheet();
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
CellRangeAddress mergedRegion = sheet.getMergedRegion(i);
if (mergedRegion.getFirstRow() == row) {
removeMergedRegion(i);
}
}
}
}
// PopupButtons
if (!sheetPopupButtons.isEmpty()) {
Map updated = new HashMap();
for (PopupButton pbutton : sheetPopupButtons.values()) {
CellReference cell = pbutton.getCellReference();
unRegisterPopupButton(pbutton);
int row = cell.getRow();
if (rowWasRemoved(row, first, n)) {
// do nothing -> will be removed
} else if (numberOfRowsAboveWasChanged(row, last, first)) {
int newRow = cell.getRow() + n;
int col = cell.getCol();
CellReference newCell = new CellReference(newRow, col, true,
true);
pbutton.setCellReference(newCell);
updated.put(newCell, pbutton);
} else {
updated.put(cell, pbutton);
}
}
sheetPopupButtons = updated;
}
// Invalid formula indicators
int activeSheetIndex = workbook.getActiveSheetIndex();
HashSet original = invalidFormulas.get(activeSheetIndex);
if (original != null) {
HashSet updated = new HashSet();
for (String key : original) {
int row = SpreadsheetUtil.getRowFromKey(key) - 1;
int col = SpreadsheetUtil.getColumnIndexFromKey(key) - 1;
if (rowWasRemoved(row, first, n)) {
// do nothing -> will be removed
} else if (numberOfRowsAboveWasChanged(row, last, first)) {
// the number of the rows above has changed -> update the
// row index
updated.add(SpreadsheetUtil.toKey(col + 1, row + n + 1));
} else {
updated.add(key);
}
}
original.clear();
invalidFormulas.put(activeSheetIndex, updated);
}
}
private boolean numberOfRowsAboveWasChanged(int row, int last, int first) {
return first <= row && row <= last;
}
private boolean rowWasRemoved(int row, int first, int n) {
return n < 0 && first + n < row && row <= first;
}
/**
* @return the common {@link FormulaEvaluator} instance.
*/
public FormulaEvaluator getFormulaEvaluator() {
return formulaEvaluator;
}
/**
* POI, as of 4.0.0, now accepts this as an argument to formula evaluation.
* Some conditional formats can modify the display text of a cell.
*
* @return the common {@link ConditionalFormattingEvaluator} instance.
*/
public ConditionalFormattingEvaluator getConditionalFormattingEvaluator() {
return conditionalFormattingEvaluator;
}
private int getLastNonBlankRow(Sheet sheet) {
for (int r = sheet.getLastRowNum(); r >= 0; r--) {
Row row = sheet.getRow(r);
if (row != null) {
for (short c = row.getFirstCellNum(); c < row
.getLastCellNum(); c++) {
Cell cell = row.getCell(c);
if (cell != null && cell.getCellType() != CellType.BLANK) {
return r;
}
}
}
}
return 0;
}
private void updateMergedRegions() {
int regions = getActiveSheet().getNumMergedRegions();
if (regions > 0) {
ArrayList _mergedRegions = new ArrayList();
for (int i = 0; i < regions; i++) {
final CellRangeAddress region = getActiveSheet()
.getMergedRegion(i);
try {
final MergedRegion mergedRegion = new MergedRegion();
mergedRegion.col1 = region.getFirstColumn() + 1;
mergedRegion.col2 = region.getLastColumn() + 1;
mergedRegion.row1 = region.getFirstRow() + 1;
mergedRegion.row2 = region.getLastRow() + 1;
mergedRegion.id = mergedRegionCounter++;
_mergedRegions.add(i, mergedRegion);
} catch (IndexOutOfBoundsException ioobe) {
createMergedRegionIntoSheet(region);
}
}
while (regions < _mergedRegions.size()) {
_mergedRegions.remove(_mergedRegions.size() - 1);
}
setMergedRegions(_mergedRegions);
} else {
setMergedRegions(null);
}
}
/**
* Deletes rows. See {@link Sheet#removeRow(Row)}. Removes all row content,
* deletes cells and resets the sheet size.
*
* Does not shift rows up (!) - use
* {@link #shiftRows(int, int, int, boolean, boolean)} for that.
*
* @param startRow
* Index of the starting row, 0-based
* @param endRow
* Index of the ending row, 0-based
*/
public void deleteRows(int startRow, int endRow) {
Sheet sheet = getActiveSheet();
for (int i = startRow; i <= endRow; i++) {
Row row = sheet.getRow(i);
if (row != null) {
getActiveSheet().removeRow(row);
}
}
float[] _rowH = Arrays.copyOf(getRowH(), getRowH().length);
for (int i = startRow; i <= endRow; i++) {
_rowH[i] = sheet.getDefaultRowHeightInPoints();
}
setRowH(_rowH);
updateMergedRegions();
valueManager.updateDeletedRowsInClientCache(startRow + 1, endRow + 1);
if (hasSheetOverlays()) {
reloadImageSizesFromPOI = true;
}
updateMarkedCells();
CellReference selectedCellReference = getSelectedCellReference();
if (selectedCellReference.getRow() >= startRow
&& selectedCellReference.getRow() <= endRow) {
selectionManager.reSelectSelectedCell();
}
}
/**
* Merges cells. See {@link Sheet#addMergedRegion(CellRangeAddress)}.
*
* @param selectionRange
* The cell range to merge, e.g. "B3:C5"
*/
public void addMergedRegion(String selectionRange) {
addMergedRegion(CellRangeAddress.valueOf(selectionRange));
}
/**
* Merge cells. See {@link Sheet#addMergedRegion(CellRangeAddress)}.
*
* @param row1
* Index of the starting row of the merged region, 0-based
* @param col1
* Index of the starting column of the merged region, 0-based
* @param row2
* Index of the ending row of the merged region, 0-based
* @param col2
* Index of the ending column of the merged region, 0-based
*/
public void addMergedRegion(int row1, int col1, int row2, int col2) {
addMergedRegion(new CellRangeAddress(row1, row2, col1, col2));
}
/**
* Merges the given cells. See
* {@link Sheet#addMergedRegion(CellRangeAddress)}.
*
* If another existing merged region is completely inside the given range,
* it is removed. If another existing region either encloses or overlaps the
* given range, an error is thrown. See
* {@link CellRangeUtil#intersect(CellRangeAddress, CellRangeAddress)}.
*
* Note: POI doesn't seem to update the cells that are "removed" due to the
* merge - the values for those cells still exist and continue being used in
* possible formulas. If you need to make sure those values are removed,
* just delete the cells before creating the merged region.
*
* If the added region affects the currently selected cell, a new
* {@link SelectionChangeEvent} is fired.
*
* @param region
* The range of cells to merge
* @throws IllegalArgumentException
* If the given region overlaps with or encloses another
* existing region within the sheet.
*/
public void addMergedRegion(CellRangeAddress region)
throws IllegalArgumentException {
final Sheet sheet = getActiveSheet();
// need to check if there are merged regions already inside the given
// range, otherwise very bad inconsistencies appear.
int index = 0;
while (index < sheet.getNumMergedRegions()) {
CellRangeAddress existingRegion = sheet.getMergedRegion(index);
int intersect = CellRangeUtil.intersect(region, existingRegion);
if (intersect == CellRangeUtil.INSIDE) {
deleteMergedRegion(index);
} else if (intersect == CellRangeUtil.OVERLAP
|| intersect == CellRangeUtil.ENCLOSES) {
throw new IllegalArgumentException("An existing region "
+ existingRegion + " "
+ (intersect == CellRangeUtil.OVERLAP ? "overlaps "
: "encloses ")
+ "the given region " + region);
} else {
index++;
}
}
createMergedRegionIntoSheet(region);
selectionManager.mergedRegionAdded(region);
}
private void createMergedRegionIntoSheet(CellRangeAddress region) {
Sheet sheet = getActiveSheet();
sheet.addMergedRegion(region);
MergedRegion mergedRegion = new MergedRegion();
mergedRegion.col1 = region.getFirstColumn() + 1;
mergedRegion.col2 = region.getLastColumn() + 1;
mergedRegion.row1 = region.getFirstRow() + 1;
mergedRegion.row2 = region.getLastRow() + 1;
mergedRegion.id = mergedRegionCounter++;
ArrayList _mergedRegions = getMergedRegions() != null
? new ArrayList<>(getMergedRegions())
: new ArrayList();
_mergedRegions.add(mergedRegion);
setMergedRegions(_mergedRegions);
// update the style & data for the region cells, effects region + 1
// FIXME POI doesn't seem to care that the other cells inside the merged
// region should be removed; the values those cells have are still used
// in formulas..
for (int r = mergedRegion.row1; r <= (mergedRegion.row2 + 1); r++) {
Row row = sheet.getRow(r - 1);
for (int c = mergedRegion.col1; c <= (mergedRegion.col2 + 1); c++) {
if (row != null) {
Cell cell = row.getCell(c - 1);
if (cell != null) {
styler.cellStyleUpdated(cell, false);
if ((c != mergedRegion.col1 || r != mergedRegion.row1)
&& c <= mergedRegion.col2
&& r <= mergedRegion.row2) {
getCellValueManager().markCellForRemove(cell);
}
}
}
}
}
styler.loadCustomBorderStylesToState();
updateMarkedCells();
}
/**
* Removes a merged region with the given index. Current merged regions can
* be inspected within the currently active sheet with
* {@link #getActiveSheet()} and {@link Sheet#getMergedRegion(int)} and
* {@link Sheet#getNumMergedRegions()}.
*
* Note that in POI after removing a merged region at index n, all regions
* added after the removed region will get a new index (index-1).
*
* If the removed region affects the currently selected cell, a new
* {@link SelectionChangeEvent} is fired.
*
* @param index
* Position of the target merged region in the POI merged region
* array, 0-based
*/
public void removeMergedRegion(int index) {
final CellRangeAddress removedRegion = getActiveSheet()
.getMergedRegion(index);
deleteMergedRegion(index);
updateMarkedCells();
// update selection if removed region overlaps
selectionManager.mergedRegionRemoved(removedRegion);
}
private void deleteMergedRegion(int index) {
final Sheet sheet = getActiveSheet();
sheet.removeMergedRegion(index);
ArrayList _mergedRegions = new ArrayList<>(
getMergedRegions());
MergedRegion mergedRegion = _mergedRegions.remove(index);
// update the style for the region cells, effects region + 1 row&col
for (int r = mergedRegion.row1; r <= (mergedRegion.row2 + 1); r++) {
Row row = sheet.getRow(r - 1);
if (row != null) {
for (int c = mergedRegion.col1; c <= (mergedRegion.col2
+ 1); c++) {
Cell cell = row.getCell(c - 1);
if (cell != null) {
styler.cellStyleUpdated(cell, false);
valueManager.markCellForUpdate(cell);
} else {
styler.clearCellStyle(r, c);
}
}
}
}
setMergedRegions(_mergedRegions);
styler.loadCustomBorderStylesToState();
}
/**
* Discards all current merged regions for the sheet and reloads them from
* the POI model.
*
* This can be used if you want to add / remove multiple merged regions
* directly from the POI model and need to update the component.
*
* Note that you must also make sure that possible styles for the merged
* regions are updated, if those were modified, by calling
* {@link #reloadActiveSheetStyles()}.
*/
public void reloadAllMergedRegions() {
SpreadsheetFactory.loadMergedRegions(this);
}
/**
* Reloads all the styles for the currently active sheet.
*/
public void reloadActiveSheetStyles() {
styler.reloadActiveSheetCellStyles();
}
/**
* Hides or shows the given column, see
* {@link Sheet#setColumnHidden(int, boolean)}.
*
* @param columnIndex
* Index of the target column, 0-based
* @param hidden
* True to hide the target column, false to show it.
*/
public void setColumnHidden(int columnIndex, boolean hidden) {
getActiveSheet().setColumnHidden(columnIndex, hidden);
ArrayList _hiddenColumnIndexes = new ArrayList<>(
getHiddenColumnIndexes());
int[] _colW = Arrays.copyOf(getColW(), getColW().length);
if (hidden && !_hiddenColumnIndexes.contains(columnIndex + 1)) {
_hiddenColumnIndexes.add(columnIndex + 1);
_colW[columnIndex] = 0;
} else if (!hidden && _hiddenColumnIndexes.contains(columnIndex + 1)) {
_hiddenColumnIndexes
.remove(_hiddenColumnIndexes.indexOf(columnIndex + 1));
_colW[columnIndex] = (int) getActiveSheet()
.getColumnWidthInPixels(columnIndex);
getCellValueManager().clearCacheForColumn(columnIndex + 1);
getCellValueManager().loadCellData(firstRow, columnIndex + 1,
lastRow, columnIndex + 1);
}
setHiddenColumnIndexes(_hiddenColumnIndexes);
setColW(_colW);
if (hasSheetOverlays()) {
reloadImageSizesFromPOI = true;
loadOrUpdateOverlays();
}
getSpreadsheetStyleFactory().reloadActiveSheetCellStyles();
}
/**
* Gets the visibility state of the given column. See
* {@link Sheet#isColumnHidden(int)}.
*
* @param columnIndex
* Index of the target column, 0-based
* @return true if the target column is hidden, false if it is visible.
*/
public boolean isColumnHidden(int columnIndex) {
return getActiveSheet().isColumnHidden(columnIndex);
}
/**
* Hides or shows the given row, see {@link Row#setZeroHeight(boolean)}.
*
* @param rowIndex
* Index of the target row, 0-based
* @param hidden
* True to hide the target row, false to show it.
*/
public void setRowHidden(int rowIndex, boolean hidden) {
final Sheet activeSheet = getActiveSheet();
Row row = activeSheet.getRow(rowIndex);
if (row == null) {
row = activeSheet.createRow(rowIndex);
}
row.setZeroHeight(hidden);
// can't assume the state already had room for the row in its
// arrays, it may have been created above. This avoids
// ArrayIndexOutOfBoundsException
SpreadsheetFactory.calculateSheetSizes(this, getActiveSheet());
if (hasSheetOverlays()) {
reloadImageSizesFromPOI = true;
loadOrUpdateOverlays();
}
getSpreadsheetStyleFactory().reloadActiveSheetCellStyles();
}
/**
* Gets the visibility state of the given row. A row is hidden when it has
* zero height, see {@link Row#getZeroHeight()}.
*
* @param rowIndex
* Index of the target row, 0-based
* @return true if the target row is hidden, false if it is visible.
*/
public boolean isRowHidden(int rowIndex) {
Row row = getActiveSheet().getRow(rowIndex);
return row == null ? false : row.getZeroHeight();
}
/**
* Reinitializes the component from the given Excel file.
*
* @param file
* Data source file. Excel format is expected.
* @throws IOException
* If the file can't be read, or the file is of an invalid
* format.
*/
public void read(File file) throws IOException {
SpreadsheetFactory.reloadSpreadsheetComponent(this, file);
}
/**
* Reinitializes the component from the given input stream. The expected
* format is that of an Excel file.
*
* @param inputStream
* Data source input stream. Excel format is expected.
* @throws IOException
* If handling the stream fails, or the data is in an invalid
* format.
*/
public void read(InputStream inputStream) throws IOException {
SpreadsheetFactory.reloadSpreadsheetComponent(this, inputStream);
}
/**
* Exports current spreadsheet into a File with the given name.
*
* @param fileName
* The full name of the file. If the name doesn't end with '.xls'
* or '.xlsx', the approriate one will be appended.
* @return A File with the content of the current {@link Workbook}, In the
* file format of the original {@link Workbook}.
* @throws FileNotFoundException
* If file name was invalid
* @throws IOException
* If the file can't be written to for any reason
*/
public File write(String fileName)
throws FileNotFoundException, IOException {
return SpreadsheetFactory.write(this, fileName);
}
/**
* Exports current spreadsheet as an output stream.
*
* @param outputStream
* The target stream
* @throws IOException
* If writing to the stream fails
*/
public void write(OutputStream outputStream) throws IOException {
SpreadsheetFactory.write(this, outputStream);
}
/**
* The row buffer size determines the amount of content rendered outside the
* top and bottom edges of the visible cell area, for smoother scrolling.
*
* Size is in pixels, the default is 200.
*
* @return The current row buffer size
*/
public int getRowBufferSize() {
return rowBufferSize;
}
/**
* Sets the row buffer size. Comes into effect the next time sheet is
* scrolled or reloaded.
*
* The row buffer size determines the amount of content rendered outside the
* top and bottom edges of the visible cell area, for smoother scrolling.
*
* @param rowBufferInPixels
* The amount of extra content rendered outside the top and
* bottom edges of the visible area.
*/
public void setRowBufferSize(int rowBufferInPixels) {
this.rowBufferSize = rowBufferInPixels;
getElement().setProperty("rowBufferSize", rowBufferInPixels);
}
/**
* The column buffer size determines the amount of content rendered outside
* the left and right edges of the visible cell area, for smoother
* scrolling.
*
* Size is in pixels, the default is 200.
*
* @return The current column buffer size
*/
public int getColBufferSize() {
return columnBufferSize;
}
/**
* Sets the column buffer size. Comes into effect the next time sheet is
* scrolled or reloaded.
*
* The column buffer size determines the amount of content rendered outside
* the left and right edges of the visible cell area, for smoother
* scrolling.
*
* @param colBufferInPixels
* The amount of extra content rendered outside the left and
* right edges of the visible area.
*/
public void setColBufferSize(int colBufferInPixels) {
columnBufferSize = colBufferInPixels;
getElement().setProperty("columnBufferSize", columnBufferSize);
}
/**
* Gets the default row count for new sheets.
*
* @return The default row count for new sheets.
*/
public int getDefaultRowCount() {
return defaultNewSheetRows;
}
/**
* Sets the default row count for new sheets.
*
* @param defaultRowCount
* The number of rows to give sheets that are created with the
* '+' button on the client side.
*/
public void setDefaultRowCount(int defaultRowCount) {
defaultNewSheetRows = defaultRowCount;
}
/**
* Gets the default column count for new sheets.
*
* @return The default column count for new sheets.
*/
public int getDefaultColumnCount() {
return defaultNewSheetColumns;
}
/**
* Sets the default column count for new sheets.
*
* @param defaultColumnCount
* The number of columns to give sheets that are created with the
* '+' button on the client side.
*/
public void setDefaultColumnCount(int defaultColumnCount) {
defaultNewSheetColumns = defaultColumnCount;
}
/**
* Call this to force the spreadsheet to reload the currently viewed cell
* contents. This forces reload of all: custom components (always visible
* and editors) from {@link SpreadsheetComponentFactory}, hyperlinks, cells'
* comments and cells' contents. Also updates styles for the visible area.
*/
public void reloadVisibleCellContents() {
loadCustomComponents();
updateRowAndColumnRangeCellData(firstRow, firstColumn, lastRow,
lastColumn);
}
/*
* (non-Javadoc)
*
* @see
* com.vaadin.server.AbstractClientConnector#setResource(java.lang.String,
* com.vaadin.server.Resource)
*
* Provides package visibility.
*/
protected void setResource(String key, StreamResource resource) {
if (resource == null) {
resources.remove(key);
getElement().removeAttribute("resource-" + key);
} else {
resources.put(key, resource.toString());
getElement().setProperty("resources",
Serializer.serialize(new ArrayList<>(resources.keySet())));
getElement().setAttribute("resource-" + key, resource);
}
}
void clearSheetServerSide() {
workbook = null;
styler = null;
valueManager.clearCachedContent();
selectionManager.clear();
historyManager.clear();
invalidFormulas.clear();
sheetPopupButtons.clear();
sheetState.clear();
clearSheetOverlays();
}
private void clearSheetOverlays() {
for (SheetOverlayWrapper image : sheetOverlays) {
removeOverlayData(image);
}
sheetOverlays.clear();
}
void setInternalWorkbook(Workbook workbook) {
this.workbook = workbook;
formulaEvaluator = workbook.getCreationHelper()
.createFormulaEvaluator();
// currently all formula implementations extend BaseFormulaEvaluator
conditionalFormattingEvaluator = new ConditionalFormattingEvaluator(
workbook, (BaseFormulaEvaluator) formulaEvaluator);
styler = createSpreadsheetStyleFactory();
reloadActiveSheetData();
if (workbook instanceof HSSFWorkbook) {
setWorkbookProtected(((HSSFWorkbook) workbook).isWriteProtected());
} else if (workbook instanceof XSSFWorkbook) {
setWorkbookProtected(((XSSFWorkbook) workbook).isStructureLocked());
}
// clear all tables from memory
tables.clear();
setVerticalScrollPositions(new int[getSheetNames().length]);
setHorizontalScrollPositions(new int[getSheetNames().length]);
conditionalFormatter = createConditionalFormatter();
setWorkbookChangeToggle(!isWorkbookChangeToggle());
}
/**
* Override this method to provide your own {@link ConditionalFormatter}
* implementation. This method is called each time we open a workbook.
*
* @return A {@link ConditionalFormatter} that is tied to this spreadsheet.
*/
protected ConditionalFormatter createConditionalFormatter() {
return new ConditionalFormatter(this);
}
/**
* Override this method to provide your own {@link SpreadsheetStyleFactory}
* implementation. This method is called each time we open a workbook.
*
* @return A {@link SpreadsheetStyleFactory} that is tied to this
* Spreadsheet.
*/
protected SpreadsheetStyleFactory createSpreadsheetStyleFactory() {
return new SpreadsheetStyleFactory(this);
}
/**
* Clears and reloads all data related to the currently active sheet.
*/
protected void reloadActiveSheetData() {
selectionManager.clear();
valueManager.clearCachedContent();
firstColumn = lastColumn = firstRow = lastRow = -1;
clearSheetOverlays();
topLeftCellCommentsLoaded = false;
Optional.ofNullable(UI.getCurrent()).ifPresent(ui -> {
ui.beforeClientResponse(this, e -> {
if (reload) {
this.updateReloadState();
}
});
});
reload = true;
setSheetIndex(
getSpreadsheetSheetIndex(workbook.getActiveSheetIndex()) + 1);
setSheetProtected(getActiveSheet().getProtect());
setCellKeysToEditorIdMap(null);
setHyperlinksTooltips(null);
setComponentIDtoCellKeysMap(null);
setOverlays(null);
setMergedRegions(null);
setCellComments(null);
setCellCommentAuthors(null);
setVisibleCellComments(null);
setInvalidFormulaCells(null);
for (Component c : customComponents) {
unRegisterCustomComponent(c);
}
customComponents.clear();
if (attachedPopupButtons != null && !attachedPopupButtons.isEmpty()) {
for (PopupButton sf : new ArrayList(
attachedPopupButtons)) {
unRegisterPopupButton(sf);
}
attachedPopupButtons.clear();
}
// clear all tables, possible tables for new/changed sheet are added
// after first round trip.
tablesLoaded = false;
reloadSheetNames();
updateMergedRegions();
styler.reloadActiveSheetColumnRowStyles();
setDisplayGridlines(getActiveSheet().isDisplayGridlines());
setDisplayRowColHeadings(getActiveSheet().isDisplayRowColHeadings());
markAsDirty();
}
private void markAsDirty() {
getElement().setProperty("dirty", System.currentTimeMillis());
}
/**
* This method should be always called when the selected cell has changed so
* proper actions can be triggered for possible custom component inside the
* cell.
*/
protected void loadCustomEditorOnSelectedCell() {
CellReference selectedCellReference = selectionManager
.getSelectedCellReference();
if (selectedCellReference != null && customComponentFactory != null) {
final short col = selectedCellReference.getCol();
final int row = selectedCellReference.getRow();
final String key = SpreadsheetUtil.toKey(col + 1, row + 1);
HashMap cellKeysToEditorIdMap = new HashMap<>(
getCellKeysToEditorIdMap());
if (cellKeysToEditorIdMap != null
&& cellKeysToEditorIdMap.containsKey(key)
&& customComponents != null) {
String componentId = cellKeysToEditorIdMap.get(key);
for (Component c : customComponents) {
if (getComponentNodeId(c).equals(componentId)) {
customComponentFactory.onCustomEditorDisplayed(
getCell(row, col), row, col, this,
getActiveSheet(), c);
return;
}
}
}
setCellKeysToEditorIdMap(cellKeysToEditorIdMap);
}
}
private void reloadSheetNames() {
final ArrayList sheetNamesList = new ArrayList();
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
if (!workbook.isSheetVeryHidden(i) && !workbook.isSheetHidden(i)) {
sheetNamesList.add(workbook.getSheetName(i));
}
}
setSheetNames(
sheetNamesList.toArray(new String[sheetNamesList.size()]));
}
/**
* Returns POI model based index for the given Spreadsheet sheet index.
*
* @param visibleSheetIndex
* Index of the sheet within this Spreadsheet, 0-based
* @return Index of the sheet within the POI model, or -1 if something went
* wrong. 0-based.
*/
public int getVisibleSheetPOIIndex(int visibleSheetIndex) {
int realIndex = -1;
int i = -1;
do {
realIndex++;
if (!workbook.isSheetVeryHidden(realIndex)
&& !workbook.isSheetHidden(realIndex)) {
i++;
}
} while (i < visibleSheetIndex
&& realIndex < (workbook.getNumberOfSheets() - 1));
return realIndex;
}
/**
* Gets the Spreadsheet sheet-index for the sheet at the given POI index.
* Index will be returned for a visible sheet only.
*
* @param poiSheetIndex
* Index of the target sheet within the POI model, 0-based
* @return Index of the target sheet in the Spreadsheet, 0-based
*/
private int getSpreadsheetSheetIndex(int poiSheetIndex) {
int ourIndex = -1;
for (int i = 0; i <= poiSheetIndex; i++) {
if (!workbook.isSheetVeryHidden(i) && !workbook.isSheetHidden(i)) {
ourIndex++;
}
}
return ourIndex;
}
/**
* Gets the protection state of the sheet at the given POI index.
*
* @param poiSheetIndex
* Index of the target sheet within the POI model, 0-based
* @return true if the target {@link Sheet} is protected, false otherwise.
*/
public boolean isSheetProtected(int poiSheetIndex) {
return workbook.getSheetAt(poiSheetIndex).getProtect();
}
/**
* Gets the protection state of the current sheet.
*
* @return true if the current {@link Sheet} is protected, false otherwise.
*/
public boolean isActiveSheetProtected() {
return isSheetProtected();
}
/**
* Gets the visibility state of the given cell.
*
* @param cell
* The cell to check
* @return true if the cell is hidden, false otherwise
*/
public boolean isCellHidden(Cell cell) {
return isActiveSheetProtected() && cell.getCellStyle().getHidden();
}
/**
* Gets the locked state of the given cell.
*
* @param cell
* The cell to check
* @return true if the cell is locked, false otherwise
*/
public boolean isCellLocked(Cell cell) {
if (isActiveSheetProtected()) {
if (cell != null) {
if (cell.getCellStyle().getIndex() != 0) {
return cell.getCellStyle().getLocked();
} else {
return getLockedColumnIndexes()
.contains(cell.getColumnIndex() + 1)
&& getLockedRowIndexes()
.contains(cell.getRowIndex() + 1);
}
} else {
return true;
}
} else {
return false;
}
}
/**
* Gets the RPC proxy for communication to the client side.
*
* @return Client RPC proxy instance
*/
protected SpreadsheetClientRpc getRpcProxy() {
return clientRpc;
}
private void updateReloadState() {
if (reload) {
setReload(reload);
reload = false;
if (initialSheetSelection == null) {
if (sheetState
.getSelectedCellsOnSheet(getActiveSheet()) == null) {
initialSheetSelection = "A1";
} else {
initialSheetSelection = sheetState
.getSelectedCellsOnSheet(getActiveSheet());
}
}
} else {
setReload(reload);
}
}
/**
* Gets the currently used style factory for this Spreadsheet.
*
* @return The current style factory.
*/
public SpreadsheetStyleFactory getSpreadsheetStyleFactory() {
return styler;
}
/**
* Note that modifications done directly with the POI {@link Workbook} API
* will not get automatically updated into the Spreadsheet component.
*
* Use {@link #markCellAsDeleted(Cell, boolean)},
* {@link #markCellAsUpdated(Cell, boolean)}, or
* {@link #reloadVisibleCellContents()} to update content.
*
* @return The currently presented workbook
*/
public Workbook getWorkbook() {
return workbook;
}
/**
* Reloads the component with the given Workbook.
*
* @param workbook
* New workbook to load
*/
public void setWorkbook(Workbook workbook) {
if (workbook == null) {
throw new NullPointerException(
"Cannot open a null workbook with Spreadsheet component.");
}
SpreadsheetFactory.reloadSpreadsheetComponent(this, workbook);
}
/**
* Note that modifications done directly with the POI {@link Sheet} API will
* not get automatically updated into the Spreadsheet component.
*
* Use {@link #markCellAsDeleted(Cell, boolean)},
* {@link #markCellAsUpdated(Cell, boolean)}, or
* {@link #reloadVisibleCellContents()} to update content.
*
* @return The currently active (= visible) sheet
*/
public Sheet getActiveSheet() {
return workbook.getSheetAt(workbook.getActiveSheetIndex());
}
/**
* Updates the given range of cells. Takes frozen panes in to account.
*
* NOTE: Does not run style updates!
*/
private void updateRowAndColumnRangeCellData(int r1, int c1, int r2,
int c2) {
// FIXME should be optimized, should not go through all links, comments
// etc. always
loadHyperLinks();
loadCellComments();
loadOrUpdateOverlays();
loadPopupButtons();
// custom components not updated here on purpose
valueManager.loadCellData(r1, c1, r2, c2);
}
/**
* Sends data of the given cell area to client side. Data is only sent once,
* unless there are changes. Cells with custom components are skipped.
*
* @param firstRow
* Index of the starting row, 1-based
* @param firstColumn
* Index of the starting column, 1-based
* @param lastRow
* Index of the ending row, 1-based
* @param lastColumn
* Index of the ending column, 1-based
*/
protected void loadCells(int firstRow, int firstColumn, int lastRow,
int lastColumn) {
loadCustomComponents();
loadHyperLinks();
loadCellComments();
loadOrUpdateOverlays();
loadTables();
loadPopupButtons();
valueManager.loadCellData(firstRow, firstColumn, lastRow, lastColumn);
loadCustomEditorOnSelectedCell();
}
void onLinkCellClick(int row, int column) {
Cell cell = getActiveSheet().getRow(row - 1).getCell(column - 1);
if (hyperlinkCellClickHandler != null) {
hyperlinkCellClickHandler.onHyperLinkCellClick(cell,
cell.getHyperlink());
}
}
void onRowResized(Map newRowSizes, int row1, int col1,
int row2, int col2) {
SizeChangeCommand command = new SizeChangeCommand(this, Type.ROW);
command.captureValues(
newRowSizes.keySet().toArray(new Integer[newRowSizes.size()]));
historyManager.addCommand(command);
for (Entry entry : newRowSizes.entrySet()) {
int index = entry.getKey();
float height = entry.getValue();
setRowHeight(index - 1, height);
}
if (hasSheetOverlays()) {
reloadImageSizesFromPOI = true;
}
loadCells(row1, col1, row2, col2);
}
/**
* Sets the row height for currently active sheet. Updates both POI model
* and the visible sheet.
*
* @param index
* Index of target row, 0-based
* @param height
* New row height in points
*/
public void setRowHeight(int index, float height) {
if (height == 0.0F) {
setRowHidden(index, true);
} else {
Row row = getActiveSheet().getRow(index);
ArrayList _hiddenRowIndexes = new ArrayList<>(
getHiddenRowIndexes());
if (_hiddenRowIndexes.contains(Integer.valueOf(index + 1))) {
_hiddenRowIndexes.remove(Integer.valueOf(index + 1));
if (row != null && row.getZeroHeight()) {
row.setZeroHeight(false);
}
}
if (row == null) {
row = getActiveSheet().createRow(index);
}
row.setHeightInPoints(height);
setHiddenRowIndexes(_hiddenRowIndexes);
// can't assume the state already had room for the row in its
// arrays, it may have been created above. This avoids
// ArrayIndexOutOfBoundsException
SpreadsheetFactory.calculateSheetSizes(this, getActiveSheet());
}
}
void onColumnResized(Map newColumnSizes, int row1,
int col1, int row2, int col2) {
SizeChangeCommand command = new SizeChangeCommand(this, Type.COLUMN);
command.captureValues(newColumnSizes.keySet()
.toArray(new Integer[newColumnSizes.size()]));
historyManager.addCommand(command);
for (Entry entry : newColumnSizes.entrySet()) {
int index = entry.getKey();
int width = entry.getValue();
setColumnWidth(index - 1, width);
}
if (hasSheetOverlays()) {
reloadImageSizesFromPOI = true;
}
loadCells(row1, col1, row2, col2);
}
/**
* Sets the column width in pixels (using conversion) for the currently
* active sheet. Updates both POI model and the visible sheet.
*
* @param index
* Index of target column, 0-based
* @param width
* New column width in pixels
*/
public void setColumnWidth(int index, int width) {
if (width == 0) {
setColumnHidden(index, true);
} else {
ArrayList _hiddenColumnIndexes = new ArrayList<>(
getHiddenColumnIndexes());
int[] _colW = Arrays.copyOf(getColW(), getColW().length);
if (_hiddenColumnIndexes.contains(Integer.valueOf(index + 1))) {
_hiddenColumnIndexes.remove(Integer.valueOf(index + 1));
}
if (getActiveSheet().isColumnHidden(index)) {
getActiveSheet().setColumnHidden(index, false);
}
_colW[index] = width;
setColW(_colW);
setHiddenColumnIndexes(_hiddenColumnIndexes);
getActiveSheet().setColumnWidth(index,
SpreadsheetUtil.pixel2WidthUnits(width));
if (getActiveSheet() instanceof XSSFSheet) {
((XSSFSheet) getActiveSheet()).getColumnHelper().cleanColumns();
}
getCellValueManager().clearCacheForColumn(index + 1);
getCellValueManager().loadCellData(firstRow, index + 1, lastRow,
index + 1);
}
}
void loadHyperLinks() {
HashMap _hyperlinksTooltips = getHyperlinksTooltips() != null
? new HashMap<>(getHyperlinksTooltips())
: null;
if (_hyperlinksTooltips == null) {
_hyperlinksTooltips = new HashMap();
} else {
_hyperlinksTooltips.clear();
}
setHyperlinksTooltips(_hyperlinksTooltips);
// removed && !topLeftCellHyperlinksLoaded as it was always false
if (getLastFrozenRow() > 0 && getLastFrozenColumn() > 0) {
loadHyperLinks(1, 1, getLastFrozenRow(), getLastFrozenColumn());
}
if (getLastFrozenRow() > 0) {
loadHyperLinks(1, firstColumn, getLastFrozenRow(), lastColumn);
}
if (getLastFrozenColumn() > 0) {
loadHyperLinks(firstRow, 1, lastRow, getLastFrozenColumn());
}
loadHyperLinks(firstRow, firstColumn, lastRow, lastColumn);
}
private void loadHyperLinks(int r1, int c1, int r2, int c2) {
HashMap _hyperlinksTooltips = getHyperlinksTooltips();
for (int r = r1 - 1; r < r2; r++) {
final Row row = getActiveSheet().getRow(r);
if (row != null) {
for (int c = c1 - 1; c < c2; c++) {
Cell cell = row.getCell(c);
if (cell != null) {
try {
Hyperlink link = cell.getHyperlink();
if (link != null) {
if (link instanceof XSSFHyperlink) {
String tooltip = ((XSSFHyperlink) link)
.getTooltip();
// Show address if no defined tooltip (like
// in
// excel)
if (tooltip == null) {
tooltip = link.getAddress();
}
_hyperlinksTooltips.put(
SpreadsheetUtil.toKey(c + 1, r + 1),
tooltip);
} else {
_hyperlinksTooltips.put(
SpreadsheetUtil.toKey(c + 1, r + 1),
link.getAddress());
}
} else {
// Check if the cell has HYPERLINK function
if (DefaultHyperlinkCellClickHandler
.isHyperlinkFormulaCell(cell)
&& hyperlinkCellClickHandler != null) {
_hyperlinksTooltips.put(
SpreadsheetUtil.toKey(c + 1, r + 1),
hyperlinkCellClickHandler
.getHyperlinkFunctionTarget(
cell));
}
}
} catch (XmlValueDisconnectedException exc) {
LOGGER.trace(exc.getMessage(), exc);
}
}
}
}
}
setHyperlinksTooltips(_hyperlinksTooltips);
}
private void loadOrUpdateOverlays() {
// Fixes the issue of overlays being lost when creating or removing
// frozen rows/columns. More like a kludge, a real solution is yet to be
// found.
if (!hasSheetOverlays()) {
SpreadsheetFactory.loadSheetOverlays(this);
}
if (hasSheetOverlays()) {
// reload images from POI because row / column sizes have changed
// currently doesn't effect anything because POI doesn't update the
// image anchor data after resizing
if (reloadImageSizesFromPOI) {
clearSheetOverlays();
SpreadsheetFactory.loadSheetOverlays(this);
reloadImageSizesFromPOI = false;
}
for (final SheetOverlayWrapper overlay : sheetOverlays) {
if (isOverlayVisible(overlay)) {
addOverlayData(overlay);
overlay.setVisible(true);
} else {
// was visible but went out of visibility
if (overlay.isVisible()) {
removeOverlayData(overlay);
overlay.setVisible(false);
}
}
}
}
}
/**
* Adds necessary data to display the overlay in the current view.
*/
private void addOverlayData(final SheetOverlayWrapper overlay) {
if (overlay.getComponent(true) != null) {
registerCustomComponent(overlay.getComponent(true));
overlayComponents.add(overlay.getComponent(true));
}
if (overlay.getId() != null && overlay.getResource() != null) {
setResource(overlay.getId(), overlay.getResource());
}
if (overlay.getId() != null) {
HashMap _overlays = getOverlays() != null
? new HashMap<>(getOverlays())
: new HashMap<>();
_overlays.put(overlay.getId(), createOverlayInfo(overlay));
setOverlays(_overlays);
overlay.setOverlayChangeListener(new OverlayChangeListener() {
@Override
public void overlayChanged() {
loadOrUpdateOverlays();
}
});
}
}
/**
* Undoes what addOverlayData did.
*/
private void removeOverlayData(final SheetOverlayWrapper overlay) {
if (overlay.getId() != null) {
if (getOverlays() != null) {
HashMap _overlays = getOverlays();
_overlays.remove(overlay.getId());
setOverlays(_overlays);
}
setResource(overlay.getId(), (StreamResource) null);
}
if (overlay.getComponent(false) != null) {
overlayComponents.remove(overlay.getComponent(false));
unRegisterCustomComponent(overlay.getComponent(false));
}
}
/**
* Decides if overlay is visible in the current view.
*/
private boolean isOverlayVisible(SheetOverlayWrapper overlay) {
var anchor = overlay.getAnchor();
// Need special handling for XSSFClientAnchor anchors of type
// DONT_MOVE_AND_RESIZE.
// See https://github.com/vaadin/flow-components/issues/3261
if (AnchorType.DONT_MOVE_AND_RESIZE.equals(anchor.getAnchorType())
&& anchor instanceof XSSFClientAnchor) {
// Since there's no way to know if an arbitrary x/y coordinate is
// inside the current viewport, always return true for these
// anchors.
return true;
}
int col1 = anchor.getCol1();
int col2 = anchor.getCol2();
int row1 = anchor.getRow1();
int row2 = anchor.getRow2();
// type=2, doesn't size with cells
final boolean isType2 = (col2 == 0 && row2 == 0);
if (!isType2) {
// to ensure compatibility with grouping/hidden columns
if (isColumnRangeHidden(col1, col2)
|| isRowRangeHidden(row1, row2)) {
return false;
}
}
int horizontalSplitPosition = getLastFrozenColumn();
int verticalSplitPosition = getLastFrozenRow();
// the sheet is divided into four areas by vertical and horizontal split
boolean visibleInArea1 = horizontalSplitPosition > 0
&& verticalSplitPosition > 0 && overlay.isVisible(1, 1,
verticalSplitPosition, horizontalSplitPosition);
boolean visibleInArea2 = horizontalSplitPosition > 0 && overlay
.isVisible(firstRow, 1, lastRow, horizontalSplitPosition);
boolean visibleInArea3 = verticalSplitPosition > 0 && overlay
.isVisible(1, firstColumn, verticalSplitPosition, lastColumn);
boolean visibleInArea4 = overlay.isVisible(firstRow, firstColumn,
lastRow, lastColumn);
return visibleInArea1 || visibleInArea2 || visibleInArea3
|| visibleInArea4;
}
/**
* Return true if all the rows in the range are hidden (including row2).
*/
private boolean isRowRangeHidden(int row1, int row2) {
for (int row = row1; row <= row2; row++) {
if (!isRowHidden(row)) {
return false;
}
}
return true;
}
/**
* Return true if all the columns in the range are hidden (including col2).
*/
private boolean isColumnRangeHidden(int col1, int col2) {
for (int col = col1; col <= col2; col++) {
if (!isColumnHidden(col)) {
return false;
}
}
return true;
}
private OverlayInfo createOverlayInfo(SheetOverlayWrapper overlayWrapper) {
OverlayInfo info = new OverlayInfo(overlayWrapper.getType());
Sheet sheet = getActiveSheet();
var anchor = overlayWrapper.getAnchor();
// Need special handling for XSSFClientAnchor anchors of type
// DONT_MOVE_AND_RESIZE.
// See https://github.com/vaadin/flow-components/issues/3261
if (AnchorType.DONT_MOVE_AND_RESIZE.equals(anchor.getAnchorType())
&& anchor instanceof XSSFClientAnchor) {
info.col = 1;
info.row = 1;
var xssfAnchor = (XSSFClientAnchor) anchor;
info.dx = (Long) xssfAnchor.getPosition().getX()
/ Units.EMU_PER_PIXEL;
info.dy = (Long) xssfAnchor.getPosition().getY()
/ Units.EMU_PER_POINT;
info.width = xssfAnchor.getSize().getCx() / Units.EMU_PER_PIXEL;
info.height = xssfAnchor.getSize().getCy() / Units.EMU_PER_POINT;
return info;
}
int col = anchor.getCol1();
while (isColumnHidden(col)) {
col++;
}
int row = anchor.getRow1();
while (isRowHidden(row)) {
row++;
}
info.col = col + 1; // 1-based
info.row = row + 1; // 1-based
info.height = overlayWrapper.getHeight(sheet, getRowH());
info.width = overlayWrapper.getWidth(sheet, getColW(), getDefColW());
// FIXME: height and width can be -1, it is never handled anywhere
// if original start row/column is hidden, use 0 dy/dx
if (col == anchor.getCol1()) {
info.dx = overlayWrapper.getDx1(sheet);
}
if (row == anchor.getRow1()) {
info.dy = overlayWrapper.getDy1(sheet);
}
return info;
}
private void loadCellComments() {
if (firstColumn == -1) {
// Spreadsheet not loaded. This method will be called again.
return;
}
HashMap _cellComments = getCellComments() != null
? new HashMap<>(getCellComments())
: null;
if (_cellComments == null) {
_cellComments = new HashMap<>();
} else {
_cellComments.clear();
}
setCellComments(_cellComments);
HashMap _cellCommentAuthors = getCellCommentAuthors() != null
? new HashMap<>(getCellCommentAuthors())
: null;
if (_cellCommentAuthors == null) {
_cellCommentAuthors = new HashMap();
} else {
_cellCommentAuthors.clear();
}
setCellCommentAuthors(_cellCommentAuthors);
ArrayList _visibleCellComments = getVisibleCellComments() != null
? new ArrayList<>(getVisibleCellComments())
: null;
if (_visibleCellComments == null) {
_visibleCellComments = new ArrayList();
} else {
_visibleCellComments.clear();
}
setVisibleCellComments(_visibleCellComments);
Set _invalidFormulaCells = getInvalidFormulaCells() != null
? new HashSet<>(getInvalidFormulaCells())
: null;
if (_invalidFormulaCells == null) {
_invalidFormulaCells = new HashSet();
} else {
_invalidFormulaCells.clear();
}
setInvalidFormulaCells(_invalidFormulaCells);
if (getLastFrozenRow() > 0 && getLastFrozenColumn() > 0
&& !topLeftCellCommentsLoaded) {
loadCellComments(1, 1, getLastFrozenRow(), getLastFrozenColumn());
}
if (getLastFrozenRow() > 0) {
loadCellComments(1, firstColumn, getLastFrozenRow(), lastColumn);
}
if (getLastFrozenColumn() > 0) {
loadCellComments(firstRow, 1, lastRow, getLastFrozenColumn());
}
loadCellComments(firstRow, firstColumn, lastRow, lastColumn);
}
private void loadCellComments(int r1, int c1, int r2, int c2) {
Sheet sheet = getActiveSheet();
HashMap _cellComments = new HashMap<>(
getCellComments());
HashMap _cellCommentAuthors = new HashMap<>(
getCellCommentAuthors());
ArrayList _visibleCellComments = new ArrayList<>(
getVisibleCellComments());
Set _invalidFormulaCells = new HashSet<>(
getInvalidFormulaCells());
for (int r = r1 - 1; r < r2; r++) {
Row row = sheet.getRow(r);
if (row != null && row.getZeroHeight()) {
continue;
}
for (int c = c1 - 1; c < c2; c++) {
if (sheet.isColumnHidden(c)) {
continue;
}
int c_one_based = c + 1;
int row_one_based = r + 1;
MergedRegion region = mergedRegionContainer
.getMergedRegion(c_one_based, row_one_based);
// do not add comments that are "below" merged regions.
// client side handles cases where comment "moves" (because
// shifting etc.) from merged cell into basic or vice versa.
if (region == null || region.col1 == c_one_based
&& region.row1 == row_one_based) {
Comment comment = sheet
.getCellComment(new CellAddress(r, c));
String key = SpreadsheetUtil.toKey(c_one_based,
row_one_based);
if (comment != null) {
// by default comments are shown when mouse is over the
// red
// triangle on the cell's top right corner. the comment
// position is calculated so that it is completely
// visible.
_cellComments.put(key, comment.getString().getString());
_cellCommentAuthors.put(key, comment.getAuthor());
if (comment.isVisible()) {
_visibleCellComments.add(key);
}
}
if (isMarkedAsInvalidFormula(c_one_based, row_one_based)) {
_invalidFormulaCells.add(key);
}
} else {
c = region.col2 - 1;
}
}
}
setCellComments(_cellComments);
setCellCommentAuthors(_cellCommentAuthors);
setVisibleCellComments(_visibleCellComments);
setInvalidFormulaCells(_invalidFormulaCells);
}
/**
* Loads the custom components for the currently viewed cells and clears
* previous components that are not currently visible.
*/
private void loadCustomComponents() {
if (customComponentFactory != null) {
HashMap _cellKeysToEditorIdMap = getCellKeysToEditorIdMap() != null
? new HashMap<>(getCellKeysToEditorIdMap())
: null;
if (_cellKeysToEditorIdMap == null) {
_cellKeysToEditorIdMap = new HashMap();
} else {
_cellKeysToEditorIdMap.clear();
}
setCellKeysToEditorIdMap(_cellKeysToEditorIdMap);
HashMap _componentIDtoCellKeysMap = getComponentIDtoCellKeysMap() != null
? new HashMap<>(getComponentIDtoCellKeysMap())
: null;
if (_componentIDtoCellKeysMap == null) {
_componentIDtoCellKeysMap = new HashMap();
} else {
_componentIDtoCellKeysMap.clear();
}
setComponentIDtoCellKeysMap(_componentIDtoCellKeysMap);
if (customComponents == null) {
customComponents = new HashSet();
}
HashSet newCustomComponents = new HashSet();
Set rowsWithComponents = new HashSet();
// iteration indexes 0-based
int verticalSplitPosition = getLastFrozenRow();
int horizontalSplitPosition = getLastFrozenColumn();
if (verticalSplitPosition > 0 && horizontalSplitPosition > 0) {
// top left pane
loadRangeComponents(newCustomComponents, rowsWithComponents, 1,
1, verticalSplitPosition, horizontalSplitPosition);
}
if (verticalSplitPosition > 0) {
// top right pane
loadRangeComponents(newCustomComponents, rowsWithComponents, 1,
firstColumn, verticalSplitPosition, lastColumn);
}
if (horizontalSplitPosition > 0) {
// bottom left pane
loadRangeComponents(newCustomComponents, rowsWithComponents,
firstRow, 1, lastRow, horizontalSplitPosition);
}
loadRangeComponents(newCustomComponents, rowsWithComponents,
firstRow, firstColumn, lastRow, lastColumn);
// unregister old
for (Iterator i = customComponents.iterator(); i
.hasNext();) {
Component c = i.next();
if (!newCustomComponents.contains(c)) {
unRegisterCustomComponent(c);
i.remove();
}
}
customComponents = newCustomComponents;
if (!rowsWithComponents.isEmpty()) {
handleRowSizes(rowsWithComponents);
}
} else {
setCellKeysToEditorIdMap(null);
setComponentIDtoCellKeysMap(null);
if (customComponents != null && !customComponents.isEmpty()) {
for (Component c : customComponents) {
unRegisterCustomComponent(c);
}
customComponents.clear();
}
handleRowSizes(new HashSet());
}
}
void loadRangeComponents(HashSet newCustomComponents,
Set rowsWithComponents, int row1, int col1, int row2,
int col2) {
HashMap _componentIDtoCellKeysMap = getComponentIDtoCellKeysMap();
HashMap _cellKeysToEditorIdMap = getCellKeysToEditorIdMap();
for (int r = row1 - 1; r < row2; r++) {
final Row row = getActiveSheet().getRow(r);
for (int c = col1 - 1; c < col2; c++) {
// Cells that are inside a merged region are skipped:
MergedRegion region = mergedRegionContainer
.getMergedRegion(c + 1, r + 1);
if (region == null
|| (region.col1 == (c + 1) && region.row1 == (r + 1))) {
Cell cell = null;
if (row != null) {
cell = row.getCell(c);
}
// check if the cell has a custom component
Component customComponent = customComponentFactory
.getCustomComponentForCell(cell, r, c, this,
getActiveSheet());
if (customComponent != null) {
final String key = SpreadsheetUtil.toKey(c + 1, r + 1);
if (!customComponents.contains(customComponent)) {
registerCustomComponent(customComponent);
}
_componentIDtoCellKeysMap
.put(getComponentNodeId(customComponent), key);
newCustomComponents.add(customComponent);
rowsWithComponents.add(r);
} else if (!isCellLocked(cell)) {
// no custom component and not locked, check if
// the cell has a custom editor
Component customEditor = customComponentFactory
.getCustomEditorForCell(cell, r, c, this,
getActiveSheet());
if (customEditor != null) {
final String key = SpreadsheetUtil.toKey(c + 1,
r + 1);
if (!newCustomComponents.contains(customEditor)
&& !customComponents
.contains(customEditor)) {
registerCustomComponent(customEditor);
}
_cellKeysToEditorIdMap.put(key,
getComponentNodeId(customEditor));
newCustomComponents.add(customEditor);
rowsWithComponents.add(r);
}
}
}
if (region != null) {
c = region.col2 - 1;
}
}
}
setCellKeysToEditorIdMap(_cellKeysToEditorIdMap);
setComponentIDtoCellKeysMap(_componentIDtoCellKeysMap);
}
private String getComponentNodeId(Component component) {
return Integer.toString(component.getElement().getNode().getId());
}
private void handleRowSizes(Set rowsWithComponents) {
// Set larger height for new rows with components
float[] _rowH = Arrays.copyOf(getRowH(), getRowH().length);
for (Integer row : rowsWithComponents) {
if (isRowHidden(row)) {
continue;
}
float currentHeight = _rowH[row];
if (currentHeight < getMinimumRowHeightForComponents()) {
_rowH[row] = getMinimumRowHeightForComponents();
}
}
// Reset row height for rows which no longer have components
if (this.rowsWithComponents != null) {
Sheet activeSheet = getActiveSheet();
for (Integer row : this.rowsWithComponents) {
if (!rowsWithComponents.contains(row)) {
if (isRowHidden(row)) {
_rowH[row] = 0;
} else {
Row r = activeSheet.getRow(row);
if (r == null) {
_rowH[row] = activeSheet
.getDefaultRowHeightInPoints();
} else {
_rowH[row] = r.getHeightInPoints();
}
}
}
}
}
setRowH(_rowH);
this.rowsWithComponents = rowsWithComponents;
}
/**
* Determines if the cell at the given coordinates is currently visible
* (rendered) in the browser.
*
* @param row
* Row index, 1-based
* @param col
* Column index, 1-based
*
* @return True if the cell is visible, false otherwise
*/
private boolean isCellVisible(int row, int col) {
int verticalSplitPosition = getLastFrozenRow();
int horizontalSplitPosition = getLastFrozenColumn();
return (col >= firstColumn && col <= lastColumn && row >= firstRow
&& row <= lastRow)
|| (col >= 1 && col <= horizontalSplitPosition && row >= 1
&& row <= verticalSplitPosition)
|| (col >= firstColumn && col <= lastColumn && row >= 1
&& row <= verticalSplitPosition)
|| (col >= 1 && col <= horizontalSplitPosition
&& row >= firstRow && row <= lastRow);
}
private void registerPopupButton(PopupButton button) {
attachedPopupButtons.add(button);
registerCustomComponent(button);
if (!getElement().equals(button.getElement().getParent())) {
getElement().appendVirtualChild(button.getElement());
}
}
private void unRegisterPopupButton(PopupButton button) {
attachedPopupButtons.remove(button);
unRegisterCustomComponent(button);
if (getElement().equals(button.getElement().getParent())) {
getElement().removeVirtualChild(button.getElement());
}
}
private void registerCustomComponent(PopupButton component) {
getElement().callJsFunction("addPopupButton",
Serializer.serialize(component.getState()));
}
private void registerCustomComponent(Component component) {
if (!getElement().equals(component.getElement().getParent())) {
getElement().appendVirtualChild(component.getElement());
}
}
private void unRegisterCustomComponent(PopupButton component) {
getElement().callJsFunction("removePopupButton",
Serializer.serialize(component.getState()));
}
private void unRegisterCustomComponent(Component component) {
Element element = component.getElement();
if (element.isVirtualChild()
&& getElement().equals(element.getParent())) {
getElement().removeVirtualChild(element);
}
}
/**
* Set a new component factory for this Spreadsheet. If a {@link Workbook}
* has been set, all components will be reloaded.
*
* @param customComponentFactory
* The new component factory to use.
*/
public void setSpreadsheetComponentFactory(
SpreadsheetComponentFactory customComponentFactory) {
this.customComponentFactory = customComponentFactory;
if (firstRow != -1) {
loadCustomComponents();
loadCustomEditorOnSelectedCell();
} else {
setCellKeysToEditorIdMap(null);
if (customComponents != null && !customComponents.isEmpty()) {
for (Component c : customComponents) {
unRegisterCustomComponent(c);
}
customComponents.clear();
}
}
}
/**
* Gets the current SpreadsheetComponentFactory.
*
* @return The currently used component factory.
*/
SpreadsheetComponentFactory getSpreadsheetComponentFactory() {
return customComponentFactory;
}
/**
* Sets a pop-up button to the given cell in the currently active sheet. If
* there is already a pop-up button in the given cell, it will be replaced.
*
* Note that if the active sheet is changed, all pop-up buttons are removed
* from the spreadsheet.
*
* @param cellAddress
* address to the target cell, e.g. "C3"
* @param popupButton
* PopupButton to set for the target cell. Passing null here
* removes the pop-up button for the target cell.
*/
public void setPopup(String cellAddress, PopupButton popupButton) {
setPopup(new CellReference(cellAddress), popupButton);
}
/**
* Sets a pop-up button to the given cell in the currently active sheet. If
* there is already a pop-up button in the given cell, it will be replaced.
*
* Note that if the active sheet is changed, all pop-up buttons are removed
* from the spreadsheet.
*
* @param row
* Row index of target cell, 0-based
* @param col
* Column index of target cell, 0-based
* @param popupButton
* PopupButton to set for the target cell. Passing null here
* removes the pop-up button for the target cell.
*/
public void setPopup(int row, int col, PopupButton popupButton) {
setPopup(new CellReference(row, col), popupButton);
}
/**
* Sets a pop-up button to the given cell in the currently active sheet. If
* there is already a pop-up button in the given cell, it will be replaced.
*
* Note that if the active sheet is changed, all pop-up buttons are removed
* from the spreadsheet.
*
* @param cellReference
* Reference to the target cell
* @param popupButton
* PopupButton to set for the target cell. Passing null here
* removes the pop-up button for the target cell.
*/
public void setPopup(CellReference cellReference, PopupButton popupButton) {
removePopupButton(cellReference);
if (popupButton != null) {
CellReference absoluteCellReference = SpreadsheetUtil
.relativeToAbsolute(this, cellReference);
popupButton.setCellReference(absoluteCellReference);
sheetPopupButtons.put(absoluteCellReference, popupButton);
if (isCellVisible(absoluteCellReference.getRow() + 1,
absoluteCellReference.getCol() + 1)) {
registerPopupButton(popupButton);
markAsDirty();
}
}
}
private void removePopupButton(CellReference cellReference) {
CellReference absoluteCellReference = SpreadsheetUtil
.relativeToAbsolute(this, cellReference);
PopupButton oldButton = sheetPopupButtons.get(absoluteCellReference);
if (oldButton != null) {
unRegisterPopupButton(oldButton);
sheetPopupButtons.remove(absoluteCellReference);
markAsDirty();
}
}
/**
* Registers and unregister pop-up button components for the currently
* visible cells.
*/
private void loadPopupButtons() {
if (sheetPopupButtons != null) {
for (PopupButton popupButton : sheetPopupButtons.values()) {
if (getActiveSheet().getSheetName().equals(
popupButton.getCellReference().getSheetName())) {
int column = popupButton.getColumn() + 1;
int row = popupButton.getRow() + 1;
if (isCellVisible(row, column)) {
registerPopupButton(popupButton);
} else {
unRegisterPopupButton(popupButton);
}
}
}
}
}
/**
* Registers the given table to this Spreadsheet, meaning that this table
* will be reloaded when the active sheet changes to the sheet containing
* the table.
*
* Populating the table content (pop-up button and other content) is the
* responsibility of the table, with {@link SpreadsheetTable#reload()}.
*
* When the sheet is changed to a different sheet than the one that the
* table belongs to, the table contents are cleared with
* {@link SpreadsheetTable#clear()}. If the table is a filtering table, the
* filters are NOT cleared (can be done with
* {@link SpreadsheetFilterTable#clearAllFilters()}.
*
* The pop-up buttons are always removed by the spreadsheet when the sheet
* changes.
*
* @param table
* The table to register
*/
public void registerTable(SpreadsheetTable table) {
tables.add(table);
if (table instanceof SpreadsheetFilterTable) {
updateAutofittedColumns((SpreadsheetFilterTable) table);
}
}
/**
* When adding a filter table, re-run autofit for columns that haven't been
* resized since the last autofit
*
* @param table
* The SpreadsheetFilterTable that was added
*/
private void updateAutofittedColumns(SpreadsheetFilterTable table) {
Sheet filteredSheet = table.getSheet();
CellRangeAddress fullTableRegion = table.getFullTableRegion();
int firstColumn = fullTableRegion.getFirstColumn();
int lastColumn = fullTableRegion.getLastColumn();
for (int i = firstColumn; i <= lastColumn; i++) {
CellReference cr = new CellReference(filteredSheet.getSheetName(),
0, i, true, true);
if (!autofittedColumnWidths.containsKey(cr)) {
continue;
}
Integer autofittedWidth = autofittedColumnWidths.get(cr);
int currentWidth = (int) filteredSheet
.getColumnWidthInPixels(cr.getCol());
// only update columns that haven't changed size since the last
// autofit
if (currentWidth == autofittedWidth) {
autofitColumn(cr.getCol());
}
}
}
/**
* Unregisters the given table from this Spreadsheet - it will no longer get
* reloaded when the sheet is changed back to the sheet containing the
* table. This does not delete any table content, use
* {@link #deleteTable(SpreadsheetTable)} to completely remove the table.
*
* See {@link #registerTable(SpreadsheetTable)}.
*
* @param table
* The table to unregister
*/
public void unregisterTable(SpreadsheetTable table) {
tables.remove(table);
}
/**
* Deletes the given table: removes it from "memory" (see
* {@link #registerTable(SpreadsheetTable)}), clears and removes all
* possible filters (if table is a {@link SpreadsheetFilterTable}), and
* clears all table pop-up buttons and content.
*
* @param table
* The table to delete
*/
public void deleteTable(SpreadsheetTable table) {
unregisterTable(table);
if (table.isTableSheetCurrentlyActive()) {
for (PopupButton popupButton : table.getPopupButtons()) {
removePopupButton(popupButton.getCellReference());
}
if (table instanceof SpreadsheetFilterTable) {
((SpreadsheetFilterTable) table).clearAllFilters();
}
table.clear();
}
}
/**
* Gets all the tables that have been registered to this Spreadsheet. See
* {@link #registerTable(SpreadsheetTable)}.
*
* @return All tables for this spreadsheet
*/
public HashSet getTables() {
return tables;
}
/**
* Gets the tables that belong to the currently active sheet (
* {@link #getActiveSheet()}). See {@link #registerTable(SpreadsheetTable)}.
*
* @return All tables for the currently active sheet
*/
public List getTablesForActiveSheet() {
List temp = new ArrayList();
for (SpreadsheetTable table : tables) {
if (table.getSheet().equals(getActiveSheet())) {
temp.add(table);
}
}
return temp;
}
/**
* Reload tables for current sheet
*/
private void loadTables() {
if (!tablesLoaded) {
for (SpreadsheetTable table : tables) {
if (table.getSheet().equals(getActiveSheet())) {
table.reload();
}
}
tablesLoaded = true;
}
}
/**
* Returns the formatted value for the given cell, using the
* {@link DataFormatter} with the current locale.
*
* See
* {@link DataFormatter#formatCellValue(Cell, FormulaEvaluator, ConditionalFormattingEvaluator)}.
*
* @param cell
* Cell to get the value from
* @return Formatted value
*/
public final String getCellValue(Cell cell) {
return valueManager.getDataFormatter().formatCellValue(cell,
valueManager.getFormulaEvaluator(),
getConditionalFormattingEvaluator());
}
/**
* Gets grid line visibility for the currently active sheet.
*
* @return True if grid lines are visible, false if they are hidden
*/
public boolean isGridlinesVisible() {
if (getActiveSheet() != null) {
return getActiveSheet().isDisplayGridlines();
}
return true;
}
/**
* Sets grid line visibility for the currently active sheet.
*
* @param visible
* True to show grid lines, false to hide them
*/
public void setGridlinesVisible(boolean visible) {
if (getActiveSheet() == null) {
throw new NullPointerException("no active sheet");
}
getActiveSheet().setDisplayGridlines(visible);
setDisplayGridlines(visible);
}
/**
* Gets row and column heading visibility for the currently active sheet.
*
* @return true if headings are visible, false if they are hidden
*/
public boolean isRowColHeadingsVisible() {
if (getActiveSheet() != null) {
return getActiveSheet().isDisplayRowColHeadings();
}
return true;
}
/**
* Sets row and column heading visibility for the currently active sheet.
*
* @param visible
* true to show headings, false to hide them
*/
public void setRowColHeadingsVisible(boolean visible) {
if (getActiveSheet() == null) {
throw new NullPointerException("no active sheet");
}
getActiveSheet().setDisplayRowColHeadings(visible);
setDisplayRowColHeadings(visible);
}
/**
* This is a parent class for a value change events.
*/
public abstract static class ValueChangeEvent
extends ComponentEvent {
private final Set changedCells;
public ValueChangeEvent(Component source,
Set changedCells) {
super(source, false);
this.changedCells = changedCells;
}
public Set getChangedCells() {
return changedCells;
}
}
/**
* This event is fired when cell value changes.
*/
public static class CellValueChangeEvent extends ValueChangeEvent {
public CellValueChangeEvent(Component source,
Set changedCells) {
super(source, changedCells);
}
}
/**
* This event is fired when the value of a cell referenced by a formula cell
* changes making the formula value change
*/
public static class FormulaValueChangeEvent extends ValueChangeEvent {
public FormulaValueChangeEvent(Component source,
Set changedCells) {
super(source, changedCells);
}
}
/**
* This event is fired when cell selection changes.
*/
public static class SelectionChangeEvent extends ComponentEvent {
private final CellReference selectedCellReference;
private final List individualSelectedCells;
private final CellRangeAddress selectedCellMergedRegion;
private final List cellRangeAddresses;
/**
* Creates a new selection change event.
*
* @param source
* Source Spreadsheet
* @param selectedCellReference
* see {@link #getSelectedCellReference()}
* @param individualSelectedCells
* see {@link #getIndividualSelectedCells()}
* @param selectedCellMergedRegion
* see {@link #getSelectedCellMergedRegion()}
* @param cellRangeAddresses
* see {@link #getCellRangeAddresses()}
*/
public SelectionChangeEvent(Component source,
CellReference selectedCellReference,
List individualSelectedCells,
CellRangeAddress selectedCellMergedRegion,
List cellRangeAddresses) {
super(source, false);
this.selectedCellReference = selectedCellReference;
this.individualSelectedCells = individualSelectedCells;
this.selectedCellMergedRegion = selectedCellMergedRegion;
this.cellRangeAddresses = cellRangeAddresses;
}
/**
* Gets the Spreadsheet where this event happened.
*
* @return Source Spreadsheet
*/
public Spreadsheet getSpreadsheet() {
return (Spreadsheet) getSource();
}
/**
* Returns reference to the currently selected single cell OR in case of
* multiple selections the last cell clicked OR in case of area select
* the cell from which the area selection was started.
*
* @return CellReference to the single selected cell, or the last cell
* selected manually (e.g. with ctrl+mouseclick)
*/
public CellReference getSelectedCellReference() {
return selectedCellReference;
}
/**
* Gets all the individually selected single cells in the current
* selection.
*
* @return All non-contiguously selected cells (e.g. with
* ctrl+mouseclick)
*/
public List getIndividualSelectedCells() {
return individualSelectedCells;
}
/**
* Gets the merged region the single selected cell is a part of, if
* applicable.
*
* @return The {@link CellRangeAddress} described the merged region the
* single selected cell is part of, if any.
*/
public CellRangeAddress getSelectedCellMergedRegion() {
return selectedCellMergedRegion;
}
/**
* Gets all separately selected cell ranges.
*
* @return All separately selected cell ranges (e.g. with
* ctrl+shift+mouseclick)
*/
public List getCellRangeAddresses() {
return cellRangeAddresses;
}
/**
* Gets a combination of all selected cells.
*
* @return A combination of all selected cells, regardless of selection
* mode. Doesn't contain duplicates.
*/
public Set getAllSelectedCells() {
return Spreadsheet.getAllSelectedCells(selectedCellReference,
individualSelectedCells, cellRangeAddresses);
}
}
private static Set getAllSelectedCells(
CellReference selectedCellReference,
List individualSelectedCells,
List cellRangeAddresses) {
Set cells = new HashSet();
for (CellReference r : individualSelectedCells) {
cells.add(r);
}
cells.add(selectedCellReference);
if (cellRangeAddresses != null) {
for (CellRangeAddress a : cellRangeAddresses) {
for (int x = a.getFirstColumn(); x <= a.getLastColumn(); x++) {
for (int y = a.getFirstRow(); y <= a.getLastRow(); y++) {
cells.add(new CellReference(y, x));
}
}
}
}
return cells;
}
/**
* Used for knowing when a user has changed the cell selection in any way.
*/
public interface SelectionChangeListener extends Serializable {
public static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionChangeListener.class, "onSelectionChange",
SelectionChangeEvent.class);
/**
* This is called when user changes cell selection.
*
* @param event
* SelectionChangeEvent that happened
*/
public void onSelectionChange(SelectionChangeEvent event);
}
/**
* Used for knowing when a user has changed the cell value in Spreadsheet
* UI.
*/
public interface CellValueChangeListener extends Serializable {
public static final Method CELL_VALUE_CHANGE_METHOD = ReflectTools
.findMethod(CellValueChangeListener.class, "onCellValueChange",
CellValueChangeEvent.class);
/**
* This is called when user changes the cell value in Spreadsheet.
*
* @param event
* CellValueChangeEvent that happened
*/
public void onCellValueChange(CellValueChangeEvent event);
}
/**
* Used for knowing when a cell referenced by a formula cell has changed in
* the Spreadsheet UI making the formula value change
*/
public interface FormulaValueChangeListener extends Serializable {
public static final Method FORMULA_VALUE_CHANGE_METHOD = ReflectTools
.findMethod(FormulaValueChangeListener.class,
"onFormulaValueChange", FormulaValueChangeEvent.class);
/**
* This is called when user changes the cell value in Spreadsheet.
*
* @param event
* FormulaValueChangeEvent that happened
*/
public void onFormulaValueChange(FormulaValueChangeEvent event);
}
/**
* Adds the given SelectionChangeListener to this Spreadsheet.
*
* @param listener
* Listener to add.
* @return a {@link Registration} for removing the event listener
*/
public Registration addSelectionChangeListener(
SelectionChangeListener listener) {
return addListener(SelectionChangeEvent.class,
listener::onSelectionChange);
}
/**
* Adds the given CellValueChangeListener to this Spreadsheet.
*
* @param listener
* Listener to add.
* @return a {@link Registration} for removing the event listener
*/
public Registration addCellValueChangeListener(
CellValueChangeListener listener) {
return addListener(CellValueChangeEvent.class,
listener::onCellValueChange);
}
/**
* Adds the given FormulaValueChangeListener to this Spreadsheet.
*
* @param listener
* Listener to add.
* @return a {@link Registration} for removing the event listener
*/
public Registration addFormulaValueChangeListener(
FormulaValueChangeListener listener) {
return addListener(FormulaValueChangeEvent.class,
listener::onFormulaValueChange);
}
/**
* An event that is fired when an attempt to modify a locked cell has been
* made.
*/
public static class ProtectedEditEvent extends ComponentEvent {
public ProtectedEditEvent(Component source) {
super(source, false);
}
}
/**
* A listener for when an attempt to modify a locked cell has been made.
*/
public interface ProtectedEditListener extends Serializable {
public static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(ProtectedEditListener.class, "writeAttempted",
ProtectedEditEvent.class);
/**
* Called when the SpreadSheet detects that the client tried to edit a
* locked cell (usually by pressing a key). Method is not called for
* each such event; instead, the SpreadSheet waits a second before
* sending a new event. This is done to give the user time to react to
* the results of this call (e.g. showing a notification).
*
* @param event
* ProtectedEditEvent that happened
*/
public void writeAttempted(ProtectedEditEvent event);
}
/**
* Add listener for when an attempt to modify a locked cell has been made.
*
* @param listener
* The listener to add.
* @return a {@link Registration} for removing the event listener
*/
public Registration addProtectedEditListener(
ProtectedEditListener listener) {
return addListener(ProtectedEditEvent.class, listener::writeAttempted);
}
/**
* Creates or removes a freeze pane from the currently active sheet.
*
* If both colSplit and rowSplit are zero then the existing freeze pane is
* removed.
*
* @param rowSplit
* Vertical position of the split, 1-based row index
* @param colSplit
* Horizontal position of the split, 1-based column index
*/
public void createFreezePane(int rowSplit, int colSplit) {
getActiveSheet().createFreezePane(colSplit, rowSplit);
SpreadsheetFactory.loadFreezePane(this);
reloadActiveSheetData();
}
/**
* Removes the freeze pane from the currently active sheet if one is
* present.
*/
public void removeFreezePane() {
PaneInformation paneInformation = getActiveSheet().getPaneInformation();
if (paneInformation != null && paneInformation.isFreezePane()) {
getActiveSheet().createFreezePane(0, 0);
SpreadsheetFactory.loadFreezePane(this);
reloadActiveSheetData();
}
}
/**
* Gets a reference to the current single selected cell.
*
* @return Reference to the currently selected single cell.
*
* NOTE: other cells might also be selected: use
* {@link #addSelectionChangeListener(SelectionChangeListener)} to
* get notified for all selection changes or call
* {@link #getSelectedCellReferences()}.
*/
public CellReference getSelectedCellReference() {
return selectionManager.getSelectedCellReference();
}
/**
* Gets all the currently selected cells.
*
* @return References to all currently selected cells.
*/
public Set getSelectedCellReferences() {
SelectionChangeEvent event = selectionManager.getLatestSelectionEvent();
if (event == null) {
return new HashSet();
} else {
return event.getAllSelectedCells();
}
}
/**
* An event that is fired to registered listeners when the selected sheet
* has been changed.
*/
public static class SheetChangeEvent extends ComponentEvent {
private final Sheet newSheet;
private final Sheet previousSheet;
private final int newSheetVisibleIndex;
private final int newSheetPOIIndex;
/**
* Creates a new SheetChangeEvent.
*
* @param source
* Spreadsheet that triggered the event
* @param newSheet
* New selection
* @param previousSheet
* Previous selection
* @param newSheetVisibleIndex
* New visible index of selection
* @param newSheetPOIIndex
* New POI index of selection
*/
public SheetChangeEvent(Component source, Sheet newSheet,
Sheet previousSheet, int newSheetVisibleIndex,
int newSheetPOIIndex) {
super(source, false);
this.newSheet = newSheet;
this.previousSheet = previousSheet;
this.newSheetVisibleIndex = newSheetVisibleIndex;
this.newSheetPOIIndex = newSheetPOIIndex;
}
/**
* Gets the newly selected sheet.
*
* @return The new selection
*/
public Sheet getNewSheet() {
return newSheet;
}
/**
* Gets the sheet that was previously selected.
*
* @return The previous selection
*/
public Sheet getPreviousSheet() {
return previousSheet;
}
/**
* Gets the index of the newly selected sheet among all visible sheets.
*
* @return Index of new selection among visible sheets
*/
public int getNewSheetVisibleIndex() {
return newSheetVisibleIndex;
}
/**
* Gets the POI index of the newly selected sheet.
*
* @return POI index of new selection
*/
public int getNewSheetPOIIndex() {
return newSheetPOIIndex;
}
}
/**
* A listener for when a sheet is selected.
*/
public interface SheetChangeListener extends Serializable {
public static final Method SHEET_CHANGE_METHOD = ReflectTools
.findMethod(SheetChangeListener.class, "onSheetChange",
SheetChangeEvent.class);
/**
* This method is called an all registered listeners when the selected
* sheet has changed.
*
* @param event
* Sheet selection event
*/
public void onSheetChange(SheetChangeEvent event);
}
/**
* Adds the given SheetChangeListener to this Spreadsheet.
*
* @param listener
* Listener to add
* @return a {@link Registration} for removing the event listener
*/
public Registration addSheetChangeListener(SheetChangeListener listener) {
return addListener(SheetChangeEvent.class, listener::onSheetChange);
}
private void fireSheetChangeEvent(Sheet previousSheet, Sheet newSheet) {
int newSheetPOIIndex = workbook.getActiveSheetIndex();
fireEvent(new SheetChangeEvent(this, newSheet, previousSheet,
getSpreadsheetSheetIndex(newSheetPOIIndex), newSheetPOIIndex));
}
/**
* This is called when the client-side connector has been initialized.
*/
protected void onConnectorInit() {
reloadCellDataOnNextScroll = true;
valueManager.clearCachedContent();
}
/**
* Reloads all data from the current spreadsheet and performs a full
* re-render.
*
* Functionally same as calling {@link #setWorkbook(Workbook)} with
* {@link #getWorkbook()} parameter.
*/
public void reload() {
setWorkbook(getWorkbook());
}
/**
* Sets the content of the status label.
*
* @param value
* The new content. Can not be HTML.
*/
public void setStatusLabelValue(String value) {
setInfoLabelValue(value);
}
/**
* Gets the content of the status label
*
* @return Current content of the status label.
*/
public String getStatusLabelValue() {
return getInfoLabelValue();
}
/**
* Selects the cell at the given coordinates
*
* @param row
* Row index, 0-based
* @param col
* Column index, 0-based
*/
public void setSelection(int row, int col) {
setSelectionRange(row, col, row, col);
}
/**
* Selects the given range, using the cell at row1 and col1 as an anchor.
*
* @param row1
* Index of the first row of the area, 0-based
* @param col1
* Index of the first column of the area, 0-based
* @param row2
* Index of the last row of the area, 0-based
* @param col2
* Index of the last column of the area, 0-based
*/
public void setSelectionRange(int row1, int col1, int row2, int col2) {
CellRangeAddress cra = new CellRangeAddress(row1, row2, col1, col2);
selectionManager.handleCellRangeSelection(cra);
}
/**
* Selects the cell(s) at the given coordinates
*
* Coordinates can be simple "A1" style addresses or ranges, named ranges,
* or a formula. Note that scatter charts, if present, use formulas that may
* contain named ranges.
*
* @param selectionRange
* The wanted range, e.g. "A3" or "B3:C5"
*/
public void setSelection(String selectionRange) {
selectionManager.handleCellRangeSelection(SpreadsheetUtil
.getRangeForReference(selectionRange, this, true));
}
/**
* Gets the ConditionalFormatter
*
* @return the {@link ConditionalFormatter} used by this {@link Spreadsheet}
*/
public ConditionalFormatter getConditionalFormatter() {
return conditionalFormatter;
}
/**
* Disposes the current {@link Workbook}, if any, and loads a new empty XSLX
* Workbook.
*
* Note: Discards all data. Be sure to write out the old Workbook if needed.
*/
public void reset() {
SpreadsheetFactory.loadNewXLSXSpreadsheet(this);
}
private CommentAuthorProvider commentAuthorProvider;
/**
* Returns the formatting string that is used when a user enters percentages
* into the Spreadsheet.
*
* Default is "0.00%".
*
* @return The formatting applied to percentage values when entered by the
* user
*/
public String getDefaultPercentageFormat() {
return defaultPercentageFormat;
}
/**
* Sets the formatting string that is used when a user enters percentages
* into the Spreadsheet.
*
* Default is "0.00%".
*/
public void setDefaultPercentageFormat(String defaultPercentageFormat) {
this.defaultPercentageFormat = defaultPercentageFormat;
}
/**
* This interface can be implemented to provide the comment author name set
* to new comments in cells.
*/
public interface CommentAuthorProvider extends Serializable {
/**
* Gets the author name for a new comment about to be added to the cell
* at the given cell reference.
*
* @param targetCell
* Reference to the target cell
* @return Comment author name
*/
public String getAuthorForComment(CellReference targetCell);
}
/**
* Sets the given CommentAuthorProvider to this Spreadsheet.
*
* @param commentAuthorProvider
* New provider
*/
public void setCommentAuthorProvider(
CommentAuthorProvider commentAuthorProvider) {
this.commentAuthorProvider = commentAuthorProvider;
}
/**
* Gets the CommentAuthorProvider currently set to this Spreadsheet.
*
* @return Current provider or null if not set.
*/
public CommentAuthorProvider getCommentAuthorProvider() {
return commentAuthorProvider;
}
/**
* Triggers editing of the cell comment in the given cell reference. Note
* that the cell must have a previously set cell comment in order to be able
* to edit it.
*
* @param cr
* Reference to the cell containing the comment to edit
*/
public void editCellComment(CellReference cr) {
getRpcProxy().editCellComment(cr.getCol(), cr.getRow());
}
/**
* Sets the visibility of the top function bar. By default the bar is
* visible.
*
* @param functionBarVisible
* True to show the top bar, false to hide it.
*/
public void setFunctionBarVisible(boolean functionBarVisible) {
if (functionBarVisible) {
removeClassName(HIDE_FUNCTION_BAR_STYLE);
} else {
addClassName(HIDE_FUNCTION_BAR_STYLE);
}
}
/**
* Gets the visibility of the top function bar. By default the bar is
* visible.
*
* @return True if the function bar is visible, false otherwise.
*/
public boolean isFunctionBarVisible() {
return !getClassNames().contains(HIDE_FUNCTION_BAR_STYLE);
}
/**
* Sets the visibility of the bottom sheet selection bar. By default the bar
* is visible.
*
* @param sheetSelectionBarVisible
* True to show the sheet selection bar, false to hide it.
*/
public void setSheetSelectionBarVisible(boolean sheetSelectionBarVisible) {
if (sheetSelectionBarVisible) {
removeClassName(HIDE_TABSHEET_STYLE);
} else {
addClassName(HIDE_TABSHEET_STYLE);
}
}
/**
* Gets the visibility of the bottom sheet selection bar. By default the bar
* is visible.
*
* @return True if the sheet selection bar is visible, false otherwise.
*/
public boolean isSheetSelectionBarVisible() {
return !getClassNames().contains(HIDE_TABSHEET_STYLE);
}
/**
* Enables or disables the report style. When enabled, the top and bottom
* bars of Spreadsheet will be hidden.
*
* @param reportStyle
* True to hide both toolbars, false to show them.
*/
public void setReportStyle(boolean reportStyle) {
setFunctionBarVisible(!reportStyle);
setSheetSelectionBarVisible(!reportStyle);
}
/**
* Gets the state of the report style.
*
* @return True if report style is enabled, false otherwise.
*/
public boolean isReportStyle() {
return !isSheetSelectionBarVisible() && !isFunctionBarVisible();
}
public void setInvalidFormulaErrorMessage(
String invalidFormulaErrorMessage) {
getElement().setProperty("invalidFormulaErrorMessage",
invalidFormulaErrorMessage);
}
/**
* Controls if a column group is collapsed or not.
*
* @param isCols
* true when collapsing columns, false
* when collapsing rows
* @param index
* A column that is part of the group, 0-based
* @param collapsed
* If the group should be collapsed or not
*/
protected void setGroupingCollapsed(boolean isCols, int index,
boolean collapsed) {
XSSFSheet activeSheet = (XSSFSheet) getActiveSheet();
if (isCols) {
if (collapsed) {
GroupingUtil.collapseColumn(activeSheet, index);
} else {
short expandLevel = GroupingUtil.expandColumn(activeSheet,
index);
updateExpandedRegion(activeSheet, index, expandLevel);
}
} else {
if (collapsed) {
GroupingUtil.collapseRow(activeSheet, index);
} else {
GroupingUtil.expandRow(activeSheet, index);
}
}
SpreadsheetFactory.calculateSheetSizes(this, activeSheet);
SpreadsheetFactory.loadGrouping(this);
reloadActiveSheetStyles();
if (hasSheetOverlays()) {
reloadImageSizesFromPOI = true;
loadOrUpdateOverlays();
}
updateMarkedCells();
}
private void updateExpandedRegion(XSSFSheet sheet, int columnIndex,
int expandLevel) {
if (expandLevel < 0) {
return;
}
int endIndex = -1;
for (GroupingData data : getColGroupingData()) {
if (data.level == expandLevel && data.startIndex <= columnIndex
&& columnIndex <= data.endIndex) {
endIndex = data.endIndex;
break;
}
}
if (endIndex < 0) {
return;
}
// update the style for the region cells, effects region + 1 row&col
int firstRowNum = sheet.getFirstRowNum();
int lastRowNum = sheet.getLastRowNum();
for (int r = firstRowNum; r <= lastRowNum; r++) {
Row row = sheet.getRow(r);
if (row != null) {
for (int c = columnIndex; c <= endIndex; c++) {
Cell cell = row.getCell(c);
if (cell != null) {
valueManager.markCellForUpdate(cell);
}
}
}
}
}
/**
* Called when a grouping level header is clicked
*
* @param isCols
* true if the user clicked on cols, false for row level headers
* @param level
* which level the user clicked
*/
protected void levelHeaderClicked(boolean isCols, int level) {
/*
* A click on a header should change groupings so that all levels above
* the selected are expanded, and the selected level is all collapsed
* (which hides any levels underneath this).
*/
if (getActiveSheet() instanceof HSSFSheet) {
return;
}
XSSFSheet xsheet = (XSSFSheet) getActiveSheet();
CTWorksheet ctWorksheet = xsheet.getCTWorksheet();
if (isCols) {
CTCols ctCols = ctWorksheet.getColsList().get(0);
List colList = ctCols.getColList();
for (CTCol col : colList) {
short l = col.getOutlineLevel();
// It's a lot easier to not call expand/collapse
if (l >= 0 && l < level) {
// expand
if (col.isSetHidden()) {
col.unsetHidden();
}
} else {
// collapse
col.setHidden(true);
}
}
} else {
/*
* Groups are more complicated than cols, use existing
* collapse/expand functionality.
*/
int lastlevel = 0;
for (int i = 0; i < getRows(); i++) {
XSSFRow row = xsheet.getRow(i);
if (row == null) {
lastlevel = 0;
continue;
}
short l = row.getCTRow().getOutlineLevel();
if (l != lastlevel) {
// group starts here
int end = (int) GroupingUtil.findEndOfRowGroup(this, i, row,
l);
long uniqueIndex = GroupingUtil.findUniqueRowIndex(this, i,
end, l);
if (l > 0 && l < level) {
// expand
GroupingUtil.expandRow(xsheet, (int) uniqueIndex);
} else if (l >= level) {
// collapse
GroupingUtil.collapseRow(xsheet, (int) uniqueIndex);
}
lastlevel = l;
}
}
}
SpreadsheetFactory.reloadSpreadsheetComponent(this, workbook);
}
void markInvalidFormula(int col, int row) {
int activeSheetIndex = workbook.getActiveSheetIndex();
if (!invalidFormulas.containsKey(activeSheetIndex)) {
invalidFormulas.put(activeSheetIndex, new HashSet<>());
}
invalidFormulas.get(activeSheetIndex)
.add(SpreadsheetUtil.toKey(col, row));
}
boolean isMarkedAsInvalidFormula(int col, int row) {
int activeSheetIndex = workbook.getActiveSheetIndex();
if (invalidFormulas.containsKey(activeSheetIndex)) {
return invalidFormulas.get(activeSheetIndex)
.contains(SpreadsheetUtil.toKey(col, row));
}
return false;
}
void removeInvalidFormulaMark(int col, int row) {
int activeSheetIndex = workbook.getActiveSheetIndex();
if (invalidFormulas.containsKey(activeSheetIndex)) {
invalidFormulas.get(activeSheetIndex)
.remove(SpreadsheetUtil.toKey(col, row));
}
}
public void addSheetOverlay(SheetOverlayWrapper image) {
sheetOverlays.add(image);
}
/**
* Get the minimum row heigth in points for the rows that contain custom
* components
*
* @return the minimum row heigths in points
*/
public int getMinimumRowHeightForComponents() {
return minimumRowHeightForComponents;
}
/***
* Set the minimum row heigth in points for the rows that contain custom
* components. If set to a small value, it might cause some components like
* checkboxes to be cut off
*
* @param minimumRowHeightForComponents
* the minimum row height in points
*/
public void setMinimumRowHeightForComponents(
final int minimumRowHeightForComponents) {
this.minimumRowHeightForComponents = minimumRowHeightForComponents;
}
/**
* This event is fired when the border of a row header is double clicked
**/
public static class RowHeaderDoubleClickEvent
extends ComponentEvent {
private final int rowIndex;
public RowHeaderDoubleClickEvent(Component source, int row) {
super(source, false);
rowIndex = row;
}
public int getRowIndex() {
return rowIndex;
}
}
/**
* Interface for listening a {@link RowHeaderDoubleClickEvent} event
**/
public interface RowHeaderDoubleClickListener extends Serializable {
Method ON_ROW_ON_ROW_HEADER_DOUBLE_CLICK = ReflectTools.findMethod(
RowHeaderDoubleClickListener.class, "onRowHeaderDoubleClick",
RowHeaderDoubleClickEvent.class);
/**
* This method is called when the user doubleclicks on the border of a
* row header
*
* @param event
* The RowHeaderDoubleClilckEvent that happened
**/
void onRowHeaderDoubleClick(RowHeaderDoubleClickEvent event);
}
/**
* This method is called when rowIndex auto-fit has been initiated from the
* browser by double-clicking the border of the target rowIndex header.
*
* @param rowIndex
* Index of the target rowIndex, 0-based
*/
protected void onRowHeaderDoubleClick(int rowIndex) {
fireRowHeaderDoubleClick(rowIndex);
}
private void fireRowHeaderDoubleClick(int rowIndex) {
fireEvent(new RowHeaderDoubleClickEvent(this, rowIndex));
}
/**
* adds a {@link RowHeaderDoubleClickListener} to the Spreadsheet
*
* @param listener
* The listener to add
* @return a {@link Registration} for removing the event listener
**/
public Registration addRowHeaderDoubleClickListener(
RowHeaderDoubleClickListener listener) {
return addListener(RowHeaderDoubleClickEvent.class,
listener::onRowHeaderDoubleClick);
}
/**
* Define the theme of the Spreadsheet.
*
* @param theme
* SpreadsheetTheme
*/
public void setTheme(SpreadsheetTheme theme) {
getElement().setAttribute("theme", theme.getThemeName());
}
/**
* Themes for the Spreadsheet.
*/
public enum SpreadsheetTheme {
LUMO("lumo"), VALO("");
private final String theme;
SpreadsheetTheme(String theme) {
this.theme = theme;
}
String getThemeName() {
return theme;
}
}
}
| | |