collabora-online/browser/src/layer/tile/CalcTileLayer.js

1136 lines
39 KiB
JavaScript

/* -*- js-indent-level: 8 -*- */
/*
* Calc tile layer is used to display a spreadsheet document
*/
/* global app */
L.CalcTileLayer = L.CanvasTileLayer.extend({
options: {
// TODO: sync these automatically from SAL_LOK_OPTIONS
sheetGeometryDataEnabled: true,
printTwipsMsgsEnabled: true,
syncSplits: true, // if false, the splits/freezes are not synced with other users viewing the same sheet.
},
twipsToHMM: function (twips) {
return (twips * 127 + 36) / 72;
},
newAnnotation: function (comment) {
var commentList = app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).sectionProperties.commentList;
var comment = null;
var cursorAddress = this._cellCursorXY;
for (var i = 0; i < commentList.length; i++) {
if (commentList[i].sectionProperties.data.tab == this._selectedPart) {
if (commentList[i].sectionProperties.data.cellRange.contains(cursorAddress)) {
comment = commentList[i];
break;
}
}
}
if (!comment) {
var pixelStart = new L.Point(Math.ceil(this._cellCursorPixels.getX1()),
Math.ceil(this._cellCursorPixels.getY1()));
var rangeStart = this.sheetGeometry.getCellFromPos(pixelStart, 'corepixels');
var pixelEnd = new L.Point(Math.floor(this._cellCursorPixels.getX2() - 1),
Math.floor(this._cellCursorPixels.getY2() - 1));
var rangeEnd = this.sheetGeometry.getCellFromPos(pixelEnd, 'corepixels');
var newComment = {
cellRange: new L.Bounds(rangeStart, rangeEnd),
anchorPos: app.file.calc.cellCursor.rectangle.twips.slice(), // Copy the array.
id: 'new',
tab: this._selectedPart,
dateTime: new Date().toDateString(),
author: this._map.getViewName(this._viewId)
};
if (app.sectionContainer.doesSectionExist('new comment')) // If adding a new comment has failed, we need to remove the leftover.
app.sectionContainer.removeSection('new comment');
comment = app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).add(newComment);
comment.show();
}
app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).modify(comment);
comment.focus();
},
beforeAdd: function (map) {
map._isCursorVisible = false;
map._addZoomLimit(this);
map.on('zoomend', this._onZoomRowColumns, this);
map.on('updateparts', this._onUpdateParts, this);
map.on('splitposchanged', this.setSplitCellFromPos, this);
map.on('commandstatechanged', this._onCommandStateChanged, this);
map.uiManager.initializeSpecializedUI('spreadsheet');
window.keyboard.hintOnscreenKeyboard(window.keyboard.onscreenKeyboardHint);
},
onAdd: function (map) {
map.addControl(L.control.tabs());
L.CanvasTileLayer.prototype.onAdd.call(this, map);
map.on('resize', function () {
if (this.isCursorVisible()) {
this._onUpdateCursor(true /* scroll */);
}
}.bind(this));
app.sectionContainer.addSection(new app.definitions.AutoFillMarkerSection());
this.insertMode = false;
this._resetInternalState();
this._sheetSwitch = new L.SheetSwitchViewRestore(map);
},
_resetInternalState: function() {
this._cellSelections = Array(0);
this._cellCursorXY = new L.Point(-1, -1);
this._gotFirstCellCursor = false;
this._lastColumn = 0; // with data
this._lastRow = 0; // with data
this.requestCellCursor();
},
isHiddenPart: function (part) {
if (!this._hiddenParts)
return false;
return this._hiddenParts.indexOf(part) !== -1;
},
hiddenParts: function () {
if (!this._hiddenParts)
return 0;
return this._hiddenParts.length;
},
hasAnyHiddenPart: function () {
if (!this._hiddenParts)
return false;
return this.hiddenParts() !== 0;
},
_onUpdateParts: function (e) {
if (typeof this._prevSelectedPart === 'number' && !e.source) {
this.refreshViewData(undefined, false /* compatDataSrcOnly */, true /* sheetGeometryChanged */);
this._switchSplitPanesContext();
}
},
_onMessage: function (textMsg, img) {
if (textMsg.startsWith('invalidateheader: column')) {
this.refreshViewData({x: this._map._getTopLeftPoint().x, y: 0,
offset: {x: undefined, y: 0}}, true /* compatDataSrcOnly */);
} else if (textMsg.startsWith('invalidateheader: row')) {
this.refreshViewData({x: 0, y: this._map._getTopLeftPoint().y,
offset: {x: 0, y: undefined}}, true /* compatDataSrcOnly */);
} else if (textMsg.startsWith('invalidateheader: all')) {
this.refreshViewData({x: this._map._getTopLeftPoint().x, y: this._map._getTopLeftPoint().y,
offset: {x: undefined, y: undefined}}, true /* compatDataSrcOnly */);
} else if (this.options.sheetGeometryDataEnabled &&
textMsg.startsWith('invalidatesheetgeometry:')) {
var params = textMsg.substring('invalidatesheetgeometry:'.length).trim().split(' ');
var flags = {};
params.forEach(function (param) {
flags[param] = true;
});
this.requestSheetGeometryData(flags);
} else if (textMsg.startsWith('printranges:')) {
this._onPrintRangesMsg(textMsg);
} else {
L.CanvasTileLayer.prototype._onMessage.call(this, textMsg, img);
}
},
// This is used to read and parse printranges so that the next
// canvas grid paint will show the visual indication of the print range
// in the current sheet if any.
_onPrintRangesMsg: function (textMsg) {
textMsg = textMsg.substr('printranges:'.length);
var msgData = JSON.parse(textMsg);
if (!msgData['printranges'] || !Array.isArray(msgData['printranges']))
return;
if (!this._printRanges) {
this._printRanges = [];
}
msgData['printranges'].forEach(function (sheetPrintRange) {
if (typeof sheetPrintRange['sheet'] !== 'number' || !Array.isArray(sheetPrintRange['ranges'])) {
return;
}
this._printRanges[sheetPrintRange['sheet']] = sheetPrintRange['ranges'];
}, this);
},
_onInvalidateTilesMsg: function (textMsg) {
var command = app.socket.parseServerCmd(textMsg);
if (command.x === undefined || command.y === undefined || command.part === undefined) {
var strTwips = textMsg.match(/\d+/g);
command.x = parseInt(strTwips[0]);
command.y = parseInt(strTwips[1]);
command.width = parseInt(strTwips[2]);
command.height = parseInt(strTwips[3]);
command.part = this._selectedPart;
}
if (isNaN(command.mode))
command.mode = this._selectedMode;
var topLeftTwips = new L.Point(command.x, command.y);
var offset = new L.Point(command.width, command.height);
var bottomRightTwips = topLeftTwips.add(offset);
if (this._debug) {
this._debugAddInvalidationRectangle(topLeftTwips, bottomRightTwips, textMsg);
}
var invalidBounds = new L.Bounds(topLeftTwips, bottomRightTwips);
var visibleArea, visiblePaneAreas;
if (this._splitPanesContext) {
visiblePaneAreas = this._splitPanesContext.getTwipsBoundList();
}
else {
var visibleTopLeft = this._latLngToTwips(this._map.getBounds().getNorthWest());
var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast());
visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight);
}
var needsNewTiles = false;
for (var key in this._tiles) {
var coords = this._tiles[key].coords;
var bounds = this._coordsToTileBounds(coords);
if (coords.part === command.part && coords.mode === command.mode &&
invalidBounds.intersects(bounds)) {
var intersectsVisible = visibleArea ? visibleArea.intersects(bounds) : bounds.intersectsAny(visiblePaneAreas);
if (intersectsVisible) {
needsNewTiles = true;
}
this._invalidateTile(key, command.wireId);
}
}
if (needsNewTiles && command.part === this._selectedPart
&& command.mode === this._selectedMode && this._debug)
{
this._debugAddInvalidationMessage(textMsg);
}
this._previewInvalidations.push(invalidBounds);
// 1s after the last invalidation, update the preview
clearTimeout(this._previewInvalidator);
this._previewInvalidator = setTimeout(L.bind(this._invalidatePreviews, this), this.options.previewInvalidationTimeout);
},
_onSetPartMsg: function (textMsg) {
var part = parseInt(textMsg.match(/\d+/g)[0]);
if (!this.isHiddenPart(part)) {
this.refreshViewData(undefined, true /* compatDataSrcOnly */, false /* sheetGeometryChanged */);
this._replayPrintTwipsMsgAllViews('cellviewcursor');
this._replayPrintTwipsMsgAllViews('textviewselection');
// Hide previous tab's shown comment (if any).
app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).hideAllComments();
this._sheetSwitch.gotSetPart(part);
}
},
_onZoomRowColumns: function () {
this._sendClientZoom();
if (this.sheetGeometry) {
this.sheetGeometry.setTileGeometryData(this._tileWidthTwips, this._tileHeightTwips,
this._tileSize);
}
this._restrictDocumentSize();
this.setSplitPosFromCell();
this._map.fire('zoomchanged');
this.refreshViewData();
this._replayPrintTwipsMsgs(false);
},
_restrictDocumentSize: function () {
if (!this.sheetGeometry) {
return;
}
var maxDocSize = this.sheetGeometry.getSize('tiletwips');
var newDocWidth = Math.min(maxDocSize.x, this._docWidthTwips);
var newDocHeight = Math.min(maxDocSize.y, this._docHeightTwips);
var lastCellPixel = this.sheetGeometry.getCellRect(this._lastColumn, this._lastRow);
var isCalcRTL = this._map._docLayer.isCalcRTL();
lastCellPixel = isCalcRTL ? lastCellPixel.getBottomRight() : lastCellPixel.getBottomLeft();
var lastCellTwips = this._corePixelsToTwips(lastCellPixel);
var mapSizeTwips = this._corePixelsToTwips(this._map.getSize());
var mapPosTwips = this._corePixelsToTwips(this._map._getTopLeftPoint());
// margin outside data area we allow to scroll
// has to be bigger on mobile to allow scroll
// to the next place where we extend that area
// (allow few mobile screens down and right)
var limitMargin = mapSizeTwips;
if (!window.mode.isDesktop()) {
limitMargin.x *= 8;
limitMargin.y *= 8;
}
var limitWidth = mapPosTwips.x + mapSizeTwips.x < lastCellTwips.x;
var limitHeight = mapPosTwips.y + mapSizeTwips.y < lastCellTwips.y;
// limit to data area only (and map size for margin)
if (limitWidth)
newDocWidth = Math.min(lastCellTwips.x + limitMargin.x, newDocWidth);
if (limitHeight)
newDocHeight = Math.min(lastCellTwips.y + limitMargin.y, newDocHeight);
var extendedLimit = false;
if (!limitWidth && maxDocSize.x > this._docWidthTwips) {
newDocWidth = this._docWidthTwips + mapSizeTwips.x;
extendedLimit = true;
}
if (!limitHeight && maxDocSize.y > this._docHeightTwips) {
newDocHeight = this._docHeightTwips + mapSizeTwips.y;
extendedLimit = true;
}
var shouldRestrict = (newDocWidth !== this._docWidthTwips ||
newDocHeight !== this._docHeightTwips);
if (!shouldRestrict) {
return;
}
// When there will be a latlng conversion, we should use CSS pixels.
var newSizePx = this._twipsToPixels(new L.Point(newDocWidth, newDocHeight));
var topLeft = this._map.unproject(new L.Point(0, 0));
var bottomRight = this._map.unproject(newSizePx);
this._docPixelSize = newSizePx.clone();
this._docWidthTwips = newDocWidth;
this._docHeightTwips = newDocHeight;
app.file.size.twips = [newDocWidth, newDocHeight];
app.file.size.pixels = [Math.round(this._tileSize * (this._docWidthTwips / this._tileWidthTwips)), Math.round(this._tileSize * (this._docHeightTwips / this._tileHeightTwips))];
app.view.size.pixels = app.file.size.pixels.slice();
this._map.setMaxBounds(new L.LatLngBounds(topLeft, bottomRight));
this._map.fire('scrolllimits', newSizePx.clone());
if (limitWidth || limitHeight || extendedLimit)
this._painter._sectionContainer.requestReDraw();
},
_getCursorPosSize: function () {
var x = -1, y = -1;
if (this._cellCursorXY) {
x = this._cellCursorXY.x + 1;
y = this._cellCursorXY.y + 1;
}
var size = new L.Point(0, 0);
if (this._cellCursor && !this._isEmptyRectangle(this._cellCursor)) {
size = this._cellCursorTwips.getSize();
}
return { curX: x, curY: y, width: size.x, height: size.y };
},
_getSelectionHeaderData: function() {
if (this._cellCSelections.empty())
return { hasSelection: false };
var bounds = this._cellCSelections.getBounds();
window.app.console.assert(bounds.isValid(), 'Non empty selection should have valid bounds');
return {
hasSelection: true,
start: this._corePixelsToTwips(bounds.min).add([1, 1]),
end: this._corePixelsToTwips(bounds.max).subtract([1, 1]),
};
},
/// take into account only data area to reduce scrollbar range
updateScollLimit: function () {
if (this.sheetGeometry && this._lastColumn && this._lastRow) {
this._restrictDocumentSize();
}
},
_handleRTLFlags: function (command) {
var rtlChanged = command.rtlParts === undefined;
rtlChanged = rtlChanged || this._rtlParts !== undefined && (
command.rtlParts.length !== this._rtlParts.length
|| this._rtlParts.some(function (part, index) {
return part !== command.rtlParts[index];
}));
this._rtlParts = command.rtlParts || [];
if (rtlChanged) {
this._adjustCanvasSectionsForLayoutChange();
}
},
_onStatusMsg: function (textMsg) {
console.log('DEBUG: onStatusMsg: ' + textMsg);
var command = app.socket.parseServerCmd(textMsg);
if (command.width && command.height && this._documentInfo !== textMsg) {
var firstSelectedPart = (typeof this._selectedPart !== 'number');
if (command.readonly === 1)
this._map.setPermission('readonly');
this._docWidthTwips = command.width;
this._docHeightTwips = command.height;
app.file.size.twips = [this._docWidthTwips, this._docHeightTwips];
app.file.size.pixels = [Math.round(this._tileSize * (this._docWidthTwips / this._tileWidthTwips)), Math.round(this._tileSize * (this._docHeightTwips / this._tileHeightTwips))];
app.view.size.pixels = app.file.size.pixels.slice();
this._docType = command.type;
this._parts = command.parts;
if (app.socket._reconnecting) {
app.socket.sendMessage('setclientpart part=' + this._selectedPart);
this._resetInternalState();
window.keyboard.hintOnscreenKeyboard(window.keyboard.onscreenKeyboardHint);
} else {
this._selectedPart = command.selectedPart;
}
this._lastColumn = command.lastcolumn;
this._lastRow = command.lastrow;
this._selectedMode = (command.mode !== undefined) ? command.mode : 0;
if (this.sheetGeometry && this._selectedPart != this.sheetGeometry.getPart()) {
// Core initiated sheet switch, need to get full sheetGeometry data for the selected sheet.
this.requestSheetGeometryData();
}
this._viewId = parseInt(command.viewid);
console.assert(this._viewId >= 0, 'Incorrect viewId received: ' + this._viewId);
var mapSize = this._map.getSize();
var sizePx = this._twipsToPixels(new L.Point(this._docWidthTwips, this._docHeightTwips));
var width = sizePx.x;
var height = sizePx.y;
if (width < mapSize.x || height < mapSize.y) {
width = Math.max(width, mapSize.x);
height = Math.max(height, mapSize.y);
var topLeft = this._map.unproject(new L.Point(0, 0));
var bottomRight = this._map.unproject(new L.Point(width, height));
this._map.setMaxBounds(new L.LatLngBounds(topLeft, bottomRight));
this._docPixelSize = {x: width, y: height};
this._map.fire('scrolllimits', {x: width, y: height});
}
else {
this._updateMaxBounds(true);
}
this._hiddenParts = command.hiddenparts || [];
this._handleRTLFlags(command);
this._documentInfo = textMsg;
var partNames = textMsg.match(/[^\r\n]+/g);
// only get the last matches
this._partNames = partNames.slice(partNames.length - this._parts);
this._map.fire('updateparts', {
selectedPart: this._selectedPart,
parts: this._parts,
docType: this._docType,
partNames: this._partNames,
hiddenParts: this._hiddenParts,
source: 'status'
});
this._resetPreFetching(true);
this._update();
if (firstSelectedPart) {
this._switchSplitPanesContext();
}
} else {
this._handleRTLFlags(command);
}
var scrollSection = app.sectionContainer.getSectionWithName(L.CSections.Scroll.name);
scrollSection.stepByStepScrolling = true;
},
// This initiates a selective repainting of row/col headers and
// gridlines based on the settings of coordinatesData.offset. This
// should be called whenever the view area changes (scrolling, panning,
// zooming, cursor moving out of view-area etc.). Depending on the
// active sheet geometry data-source, it may ask core to send current
// view area's data or the global data on geometry changes.
refreshViewData: function (coordinatesData, compatDataSrcOnly, sheetGeometryChanged) {
if (this.options.sheetGeometryDataEnabled && compatDataSrcOnly) {
return;
}
// There are places that call this function with no arguments to indicate that the
// command arguments should be the current map area coordinates.
if (typeof coordinatesData != 'object') {
coordinatesData = {};
}
var offset = coordinatesData.offset || {};
var topLeftPoint = new L.Point(coordinatesData.x, coordinatesData.y);
var sizePx = this._map.getSize();
if (topLeftPoint.x === undefined) {
topLeftPoint.x = this._map._getTopLeftPoint().x;
}
if (topLeftPoint.y === undefined) {
topLeftPoint.y = this._map._getTopLeftPoint().y;
}
var updateRows = true;
var updateCols = true;
if (offset.x === 0) {
updateCols = false;
if (!this.options.sheetGeometryDataEnabled) {
topLeftPoint.x = -1;
sizePx.x = 0;
}
}
if (offset.y === 0) {
updateRows = false;
if (!this.options.sheetGeometryDataEnabled) {
topLeftPoint.y = -1;
sizePx.y = 0;
}
}
var pos = this._pixelsToTwips(topLeftPoint);
var size = this._pixelsToTwips(sizePx);
if (!this.options.sheetGeometryDataEnabled) {
this.requestViewRowColumnData(pos, size);
return;
}
if (sheetGeometryChanged || !this.sheetGeometry) {
this.requestSheetGeometryData(
{columns: updateCols, rows: updateRows});
return;
}
if (this.sheetGeometry) {
this.sheetGeometry.setViewArea(pos, size);
this._updateHeadersGridLines(undefined, updateCols, updateRows);
}
},
// This send .uno:ViewRowColumnHeaders command to core with the new view coordinates (tile-twips).
requestViewRowColumnData: function (pos, size) {
var payload = 'commandvalues command=.uno:ViewRowColumnHeaders?x=' + Math.round(pos.x) + '&y=' + Math.round(pos.y) +
'&width=' + Math.round(size.x) + '&height=' + Math.round(size.y);
app.socket.sendMessage(payload);
},
// sends the .uno:SheetGeometryData command optionally with arguments.
requestSheetGeometryData: function (flags) {
if (!this.sheetGeometry) {
// Suppress multiple requests at document load, till we get a response.
if (this._sheetGeomFirstWait === true) {
return;
}
this._sheetGeomFirstWait = true;
}
var unoCmd = '.uno:SheetGeometryData';
var haveArgs = (typeof flags == 'object' &&
(flags.columns === true || flags.rows === true || flags.all === true));
var payload = 'commandvalues command=' + unoCmd;
if (haveArgs) {
var argList = [];
var both = (flags.all === true);
if (both || flags.columns === true) {
argList.push('columns=1');
}
if (both || flags.rows === true) {
argList.push('rows=1');
}
var dataTypeFlagNames = ['sizes', 'hidden', 'filtered', 'groups'];
var dataTypesPresent = false;
dataTypeFlagNames.forEach(function (name) {
if (flags[name] === true) {
argList.push(name + '=1');
dataTypesPresent = true;
}
});
if (!dataTypesPresent) {
dataTypeFlagNames.forEach(function (name) {
argList.push(name + '=1');
});
}
payload += '?' + argList.join('&');
}
app.socket.sendMessage(payload);
},
// Sends a notification to the row/col header and gridline controls that
// they need repainting.
// viewAreaData is the parsed .uno:ViewRowColumnHeaders JSON if that source is used.
// else it should be undefined.
_updateHeadersGridLines: function (viewAreaData, updateCols, updateRows) {
this._map.fire('viewrowcolumnheaders', {
data: viewAreaData,
updaterows: updateRows,
updatecolumns: updateCols,
cursor: this._getCursorPosSize(),
selection: this._getSelectionHeaderData(),
context: this
});
},
_addRemoveGroupSections: function () {
// If there are row and column groups at the same time, add CornerGroup section.
if (this.sheetGeometry._rows._outlines._outlines.length > 0 && this.sheetGeometry._columns._outlines._outlines.length > 0) {
if (!this._painter._sectionContainer.doesSectionExist(L.CSections.CornerGroup.name))
this._painter._sectionContainer.addSection(L.control.cornerGroup());
}
else { // If not, remove CornerGroup section.
this._painter._sectionContainer.removeSection(L.CSections.CornerGroup.name);
}
// If there are row groups, add RowGroup section.
if (this.sheetGeometry._rows._outlines._outlines.length > 0) {
if (!this._painter._sectionContainer.doesSectionExist(L.CSections.RowGroup.name))
this._painter._sectionContainer.addSection(L.control.rowGroup());
}
else { // If not, remove RowGroup section.
this._painter._sectionContainer.removeSection(L.CSections.RowGroup.name);
}
// If there are column groups, add ColumnGroup section.
if (this.sheetGeometry._columns._outlines._outlines.length > 0) {
if (!this._painter._sectionContainer.doesSectionExist(L.CSections.ColumnGroup.name)) {
this._painter._sectionContainer.addSection(L.control.columnGroup());
this._painter._sectionContainer.canvas.style.border = '1px solid darkgrey';
}
}
else { // If not, remove ColumnGroup section.
this._painter._sectionContainer.removeSection(L.CSections.ColumnGroup.name);
this._painter._sectionContainer.canvas.style.border = '0px solid darkgrey';
}
},
_adjustCanvasSectionsForLayoutChange: function () {
var sheetIsRTL = this._rtlParts.indexOf(this._selectedPart) >= 0;
if (sheetIsRTL && this._layoutIsRTL !== true) {
console.log('debug: in LTR -> RTL canvas section adjustments');
var sectionContainer = this._painter._sectionContainer;
var tilesSection = sectionContainer.getSectionWithName(L.CSections.Tiles.name);
var rowHeaderSection = sectionContainer.getSectionWithName(L.CSections.RowHeader.name);
var columnHeaderSection = sectionContainer.getSectionWithName(L.CSections.ColumnHeader.name);
var cornerHeaderSection = sectionContainer.getSectionWithName(L.CSections.CornerHeader.name);
var columnGroupSection = sectionContainer.getSectionWithName(L.CSections.ColumnGroup.name);
var rowGroupSection = sectionContainer.getSectionWithName(L.CSections.RowGroup.name);
var cornerGroupSection = sectionContainer.getSectionWithName(L.CSections.CornerGroup.name);
// Scroll section covers the entire document area, and needs RTL adjustments internally.
if (cornerGroupSection) {
cornerGroupSection.anchor = ['top', 'right'];
}
if (rowGroupSection) {
rowGroupSection.anchor = [[L.CSections.CornerGroup.name, 'bottom', 'top'], 'right'];
}
if (columnGroupSection) {
columnGroupSection.anchor = ['top', [L.CSections.CornerGroup.name, '-left', 'right']];
}
cornerHeaderSection.anchor = [[L.CSections.ColumnGroup.name, 'bottom', 'top'], [L.CSections.RowGroup.name, '-left', 'right']];
rowHeaderSection.anchor = [[L.CSections.CornerHeader.name, 'bottom', 'top'], [L.CSections.RowGroup.name, '-left', 'right']];
columnHeaderSection.anchor = [[L.CSections.ColumnGroup.name, 'bottom', 'top'], [L.CSections.CornerHeader.name, '-left', 'right']];
columnHeaderSection.expand = ['left'];
tilesSection.anchor = [[L.CSections.ColumnHeader.name, 'bottom', 'top'], [L.CSections.RowHeader.name, '-left', 'right']];
this._layoutIsRTL = true;
sectionContainer.reNewAllSections(true);
this._syncTileContainerSize();
} else if (!sheetIsRTL && this._layoutIsRTL === true) {
console.log('debug: in RTL -> LTR canvas section adjustments');
this._layoutIsRTL = false;
var sectionContainer = this._painter._sectionContainer;
var tilesSection = sectionContainer.getSectionWithName(L.CSections.Tiles.name);
var rowHeaderSection = sectionContainer.getSectionWithName(L.CSections.RowHeader.name);
var columnHeaderSection = sectionContainer.getSectionWithName(L.CSections.ColumnHeader.name);
var cornerHeaderSection = sectionContainer.getSectionWithName(L.CSections.CornerHeader.name);
var columnGroupSection = sectionContainer.getSectionWithName(L.CSections.ColumnGroup.name);
var rowGroupSection = sectionContainer.getSectionWithName(L.CSections.RowGroup.name);
var cornerGroupSection = sectionContainer.getSectionWithName(L.CSections.CornerGroup.name);
if (cornerGroupSection) {
cornerGroupSection.anchor = ['top', 'left'];
}
if (rowGroupSection) {
rowGroupSection.anchor = [[L.CSections.CornerGroup.name, 'bottom', 'top'], 'left'];
}
if (columnGroupSection) {
columnGroupSection.anchor = ['top', [L.CSections.CornerGroup.name, 'right', 'left']];
}
cornerHeaderSection.anchor = [[L.CSections.ColumnGroup.name, 'bottom', 'top'], [L.CSections.RowGroup.name, 'right', 'left']];
rowHeaderSection.anchor = [[L.CSections.CornerHeader.name, 'bottom', 'top'], [L.CSections.RowGroup.name, 'right', 'left']];
columnHeaderSection.anchor = [[L.CSections.ColumnGroup.name, 'bottom', 'top'], [L.CSections.CornerHeader.name, 'right', 'left']];
columnHeaderSection.expand = ['right'];
tilesSection.anchor = [[L.CSections.ColumnHeader.name, 'bottom', 'top'], [L.CSections.RowHeader.name, 'right', 'left']];
sectionContainer.reNewAllSections(true);
this._syncTileContainerSize();
}
},
_handleSheetGeometryDataMsg: function (jsonMsgObj, differentSheet) {
if (!this.sheetGeometry) {
this._sheetGeomFirstWait = false;
this.sheetGeometry = new L.SheetGeometry(jsonMsgObj,
this._tileWidthTwips, this._tileHeightTwips,
this._tileSize, this._selectedPart);
this._painter._sectionContainer.addSection(L.control.cornerHeader());
this._painter._sectionContainer.addSection(L.control.rowHeader());
this._painter._sectionContainer.addSection(L.control.columnHeader());
}
else {
this.sheetGeometry.update(jsonMsgObj, /* checkCompleteness */ false, this._selectedPart);
}
this._replayPrintTwipsMsgs(differentSheet);
this.sheetGeometry.setViewArea(this._pixelsToTwips(this._map._getTopLeftPoint()),
this._pixelsToTwips(this._map.getSize()));
this._addRemoveGroupSections();
console.log('debug: got sheetGeometry: this._rtlParts = ' + this._rtlParts + ' this._selectedPart = ' + this._selectedPart);
this._adjustCanvasSectionsForLayoutChange();
this._updateHeadersGridLines(undefined, true /* updateCols */,
true /* updateRows */);
this.dontSendSplitPosToCore = true;
this.setSplitPosFromCell();
this.dontSendSplitPosToCore = false;
this._syncTileContainerSize();
this._map.fire('sheetgeometrychanged');
},
// Calculates the split position in (core-pixels) from the split-cell.
setSplitPosFromCell: function (forceSplittersUpdate) {
if (!this.sheetGeometry || !this._splitPanesContext) {
return;
}
this._splitPanesContext.setSplitPosFromCell(forceSplittersUpdate);
},
// Calculates the split-cell from the split position in (core-pixels).
setSplitCellFromPos: function () {
if (!this.sheetGeometry || !this._splitPanesContext) {
return;
}
this._splitPanesContext.setSplitCellFromPos();
},
_switchSplitPanesContext: function () {
if (!this.hasSplitPanesSupport()) {
return;
}
if (!this._splitPaneCache) {
this._splitPaneCache = {};
}
window.app.console.assert(typeof this._selectedPart === 'number', 'invalid selectedPart');
var spContext = this._splitPaneCache[this._selectedPart];
if (!spContext) {
spContext = new L.CalcSplitPanesContext(this);
this._splitPaneCache[this._selectedPart] = spContext;
}
this._splitPanesContext = spContext;
if (this.sheetGeometry) {
// Force update of the splitter lines.
this.setSplitPosFromCell(true);
}
},
_onRowColSelCount: function (state) {
if (state.trim() !== '') {
var rowCount = parseInt(state.split(', ')[0].trim().split(' ')[0].replace(',', '').replace(',', ''));
var columnCount = parseInt(state.split(', ')[1].trim().split(' ')[0].replace(',', '').replace(',', ''));
if (rowCount > 1000000)
this._map.wholeColumnSelected = true;
else
this._map.wholeColumnSelected = false;
if (columnCount === 1024)
this._map.wholeRowSelected = true;
else
this._map.wholeRowSelected = false;
}
else {
this._map.wholeColumnSelected = false;
this._map.wholeRowSelected = false;
}
},
_onCommandStateChanged: function (e) {
if (e.commandName === '.uno:FreezePanesColumn') {
this._onSplitStateChanged(e, true /* isSplitCol */);
}
else if (e.commandName === '.uno:FreezePanesRow') {
this._onSplitStateChanged(e, false /* isSplitCol */);
}
else if (e.commandName === '.uno:RowColSelCount') {
// We also call the function when state is empty, because row/column variables should be set.
if (e.state.trim() === '' || e.state.startsWith('Selected'))
this._onRowColSelCount(e.state.replace('Selected:', '').replace('row', '').replace('column', '').replace('s', ''));
}
else if (e.commandName === '.uno:InsertMode') {
/* If we get textselection message from core:
When insertMode is active: User is selecting some text.
When insertMode is passive: User is selecting cells.
*/
this.insertMode = e.state.trim() === '' ? false: true;
}
},
_onSplitStateChanged: function (e, isSplitCol) {
if (!this._splitPanesContext) {
return;
}
if (!this._splitCellState) {
this._splitCellState = new L.Point(-1, -1);
}
if (!e.state || e.state.length === 0) {
window.app.console.warn('Empty argument for ' + e.commandName);
return;
}
var values = e.state.split('/');
var newSplitIndex = Math.floor(parseInt(values[0]));
window.app.console.assert(!isNaN(newSplitIndex) && newSplitIndex >= 0, 'invalid argument for ' + e.commandName);
// This stores the current split-cell state of core, so this should not be modified.
this._splitCellState[isSplitCol ? 'x' : 'y'] = newSplitIndex;
if (!this.options.syncSplits) {
return;
}
var changed = isSplitCol ? this._splitPanesContext.setSplitCol(newSplitIndex) :
this._splitPanesContext.setSplitRow(newSplitIndex);
if (changed) {
this.setSplitPosFromCell();
}
},
sendSplitIndex: function (newSplitIndex, isSplitCol) {
if (!this._map.isEditMode() || !this._splitCellState || !this.options.syncSplits) {
return false;
}
var splitColState = this._splitCellState.x;
var splitRowState = this._splitCellState.y;
if (splitColState === -1 || splitRowState === -1) {
// Did not get the 'first' FreezePanesColumn/FreezePanesRow messages from core yet.
return false;
}
var currentState = isSplitCol ? splitColState : splitRowState;
if (currentState === newSplitIndex) {
return false;
}
var unoName = isSplitCol ? 'FreezePanesColumn' : 'FreezePanesRow';
var command = {};
command[unoName] = {
type: 'int32',
value: newSplitIndex
};
this._map.sendUnoCommand('.uno:' + unoName, command);
return true;
},
_onCommandValuesMsg: function (textMsg) {
var jsonIdx = textMsg.indexOf('{');
if (jsonIdx === -1)
return;
var values = JSON.parse(textMsg.substring(jsonIdx));
if (!values) {
return;
}
var comment;
if (values.commandName === '.uno:ViewRowColumnHeaders') {
this._updateHeadersGridLines(values);
} else if (values.commandName === '.uno:SheetGeometryData') {
var differentSheet = this.sheetGeometry === undefined || this._selectedPart !== this.sheetGeometry.getPart();
// duplicate sheet-geometry for same sheet triggers replay of other messages that
// disrupt the view restore during sheet switch.
if (this._oldSheetGeomMsg === textMsg && !differentSheet)
return;
this._oldSheetGeomMsg = textMsg;
this._handleSheetGeometryDataMsg(values, differentSheet);
} else if (values.comments) {
app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).clearList();
app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).importComments(values.comments);
} else if (values.commentsPos) {
var section = app.sectionContainer.getSectionWithName(L.CSections.CommentList.name);
// invalidate all comments
section.sectionProperties.commentList.forEach(function (comment) {
comment.valid = false;
});
for (var index in values.commentsPos) {
comment = values.commentsPos[index];
if (section)
{
var commentObject;
for (var i = 0; i < section.sectionProperties.commentList.length; i++) {
if (parseInt(section.sectionProperties.commentList[i].sectionProperties.data.id) === parseInt(comment.id)) {
if (parseInt(section.sectionProperties.commentList[i].sectionProperties.data.tab) === parseInt(comment.tab)) {
commentObject = section.sectionProperties.commentList[i];
} else {
// tabs can be moved around and we need to update the tab because the id is still valid.
commentObject = section.sectionProperties.commentList[i];
commentObject.sectionProperties.data.tab = comment.tab;
}
commentObject.valid = true;
break;
}
}
if (commentObject) {
// turn cell range string into Bounds
commentObject.sectionProperties.data.cellRange = this._parseCellRange(comment.cellRange);
}
}
}
if (section)
section.onCommentsDataUpdate();
} else {
L.CanvasTileLayer.prototype._onCommandValuesMsg.call(this, textMsg);
}
},
_onTextSelectionMsg: function (textMsg) {
L.CanvasTileLayer.prototype._onTextSelectionMsg.call(this, textMsg);
// If this is a cellSelection message, user shouldn't be editing a cell. Below check is for ensuring that.
if (this.insertMode === false && this._cellCursorXY && this._cellCursorXY.x !== -1) {
// When insertMode is false, this is a cell selection message.
textMsg = textMsg.replace('textselection:', '');
if (textMsg.trim() !== 'EMPTY' && textMsg.trim() !== '') {
this._cellSelections = textMsg.split(';');
var ratio = this._tileSize / this._tileWidthTwips;
var that = this;
this._cellSelections = this._cellSelections.map(function(element) {
element = element.split(',');
var topLeftTwips = new L.Point(parseInt(element[0]), parseInt(element[1]));
var offset = new L.Point(parseInt(element[2]), parseInt(element[3]));
var bottomRightTwips = topLeftTwips.add(offset);
var boundsTwips = that._convertToTileTwipsSheetArea(new L.Bounds(topLeftTwips, bottomRightTwips));
element = L.LOUtil.createRectangle(boundsTwips.min.x * ratio, boundsTwips.min.y * ratio, boundsTwips.getSize().x * ratio, boundsTwips.getSize().y * ratio);
return element;
});
}
else {
this._cellSelections = Array(0);
}
this._refreshRowColumnHeaders();
}
},
_onCellCursorMsg: function (textMsg) {
L.CanvasTileLayer.prototype._onCellCursorMsg.call(this, textMsg);
this._refreshRowColumnHeaders();
if (!this._gotFirstCellCursor) {
// Drawing is disabled from CalcTileLayer construction, enable it now.
this._gotFirstCellCursor = true;
this._update();
this.enableDrawing();
}
},
_getEditCursorRectangle: function (msgObj) {
if (!this.options.printTwipsMsgsEnabled || !this.sheetGeometry ||
!Object.prototype.hasOwnProperty.call(msgObj, 'relrect') || !Object.prototype.hasOwnProperty.call(msgObj, 'refpoint')) {
// 1) non-print-twips messaging mode OR
// 2) the edit-cursor belongs to draw/chart objects.
return L.CanvasTileLayer.prototype._getEditCursorRectangle.call(this, msgObj);
}
if (typeof msgObj !== 'object') {
window.app.console.error('invalid edit cursor message');
return undefined;
}
var relrect = L.Bounds.parse(msgObj.relrect);
var refpoint = L.Point.parse(msgObj.refpoint);
refpoint = this.sheetGeometry.getTileTwipsPointFromPrint(refpoint);
return relrect.add(refpoint);
},
_getTextSelectionRectangles: function (textMsg) {
if (!this.options.printTwipsMsgsEnabled || !this.sheetGeometry) {
return L.CanvasTileLayer.prototype._getTextSelectionRectangles.call(this, textMsg);
}
if (typeof textMsg !== 'string') {
window.app.console.error('invalid text selection message');
return [];
}
var refpointDelim = '::';
var delimIndex = textMsg.indexOf(refpointDelim);
if (delimIndex === -1) {
// No refpoint information available, treat it as cell-range selection rectangle.
var rangeRectArray = L.Bounds.parseArray(textMsg);
rangeRectArray = rangeRectArray.map(function (rect) {
return this._convertToTileTwipsSheetArea(rect);
}, this);
return rangeRectArray;
}
var refpoint = L.Point.parse(textMsg.substring(delimIndex + refpointDelim.length));
refpoint = this.sheetGeometry.getTileTwipsPointFromPrint(refpoint);
var rectArray = L.Bounds.parseArray(textMsg.substring(0, delimIndex));
rectArray.forEach(function (rect) {
rect._add(refpoint); // compute absolute coordinates and update in-place.
});
return rectArray;
},
getSnapDocPosX: function (docPosX, unit) {
if (!this.options.sheetGeometryDataEnabled) {
return docPosX;
}
unit = unit || 'corepixels';
return this.sheetGeometry.getSnapDocPosX(docPosX, unit);
},
getSnapDocPosY: function (docPosY, unit) {
if (!this.options.sheetGeometryDataEnabled) {
return docPosY;
}
unit = unit || 'corepixels';
return this.sheetGeometry.getSnapDocPosY(docPosY, unit);
},
getSplitPanesContext: function () {
if (!this.hasSplitPanesSupport()) {
return undefined;
}
return this._splitPanesContext;
},
getMaxDocSize: function () {
if (this.sheetGeometry) {
return this.sheetGeometry.getSize('corepixels');
}
return this._twipsToPixels(new L.Point(this._docWidthTwips, this._docHeightTwips));
},
getCursorPos: function () {
return this._twipsToPixels(this._cellCursorTwips.getTopLeft());
},
_calculateScrollForNewCellCursor: function () {
var scroll = new L.LatLng(0, 0);
if (!this._cellCursor || this._isEmptyRectangle(this._cellCursor)) {
return scroll;
}
var map = this._map;
var paneRectsInLatLng = this.getPaneLatLngRectangles();
if (this._cellCursor.isInAny(paneRectsInLatLng)) {
return scroll; // no scroll needed.
}
var freePaneBounds = paneRectsInLatLng[paneRectsInLatLng.length - 1];
var splitPoint = map.unproject(this._splitPanesContext ? this._splitPanesContext.getSplitPos() : new L.Point(0, 0));
if (this._cellCursor.getEast() > splitPoint.lng) {
var freePaneWidth = Math.abs(freePaneBounds.getEast() - freePaneBounds.getWest());
var cursorWidth = Math.abs(this._cellCursor.getEast() - this._cellCursor.getWest());
var spacingX = cursorWidth / 4.0;
if (this._cellCursor.getWest() < freePaneBounds.getWest()) {
scroll.lng = this._cellCursor.getWest() - freePaneBounds.getWest() - spacingX;
}
else if (cursorWidth < freePaneWidth && this._cellCursor.getEast() > freePaneBounds.getEast()) {
scroll.lng = this._cellCursor.getEast() - freePaneBounds.getEast() + spacingX;
}
}
if (this._cellCursor.getSouth() < splitPoint.lat) {
var spacingY = Math.abs((this._cellCursor.getSouth() - this._cellCursor.getNorth())) / 4.0;
if (this._cellCursor.getNorth() > freePaneBounds.getNorth()) {
scroll.lat = this._cellCursor.getNorth() - freePaneBounds.getNorth() + spacingY;
}
else if (this._cellCursor.getSouth() < freePaneBounds.getSouth()) {
scroll.lat = this._cellCursor.getSouth() - freePaneBounds.getSouth() - spacingY;
}
}
return scroll;
},
getSelectedPart: function () {
return this._selectedPart;
},
});