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

7581 lines
246 KiB
JavaScript

/* -*- js-indent-level: 8; fill-column: 100 -*- */
/*
* L.CanvasTileLayer is a layer with canvas based rendering.
*/
/* global app L CanvasSectionContainer CanvasOverlay CDarkOverlay CSplitterLine $ _ CPointSet CPolyUtil CPolygon Cursor CCellCursor CCellSelection PathGroupType UNOKey UNOModifier Uint8ClampedArray Uint8Array Uint32Array */
/*eslint no-extend-native:0*/
if (typeof String.prototype.startsWith !== 'function') {
String.prototype.startsWith = function (str) {
return this.slice(0, str.length) === str;
};
}
// debugging aid.
function hex2string(inData)
{
var hexified = [];
var data = new Uint8Array(inData);
for (var i = 0; i < data.length; i++) {
var hex = data[i].toString(16);
var paddedHex = ('00' + hex).slice(-2);
hexified.push(paddedHex);
}
return hexified.join('');
}
function clamp(num, min, max)
{
return Math.min(Math.max(num, min), max);
}
// CStyleData is used to obtain CSS property values from style data
// stored in DOM elements in the form of custom CSS properties/variables.
var CStyleData = L.Class.extend({
initialize: function (styleDataDiv) {
this._div = styleDataDiv;
},
getPropValue: function (name) {
return getComputedStyle(this._div).getPropertyValue(name);
},
getIntPropValue: function(name) { // (String) -> Number
return parseInt(this.getPropValue(name));
},
getFloatPropValue: function(name) { // (String) -> Number
return parseFloat(this.getPropValue(name));
},
getFloatPropWithoutUnit: function(name) { // (String) -> Number
var value = this.getPropValue(name);
if (value.indexOf('px'))
value = value.split('px')[0];
return parseFloat(value);
}
});
// CSelections is used to add/modify/clear selections (text/cell-area(s)/ole)
// on canvas using polygons (CPolygon).
var CSelections = L.Class.extend({
initialize: function (pointSet, canvasOverlay, selectionsDataDiv, map, isView, viewId, selectionType) {
this._pointSet = pointSet ? pointSet : new CPointSet();
this._overlay = canvasOverlay;
this._styleData = new CStyleData(selectionsDataDiv);
this._map = map;
this._name = 'selections' + (isView ? '-viewid-' + viewId : '');
this._isView = isView;
this._viewId = viewId;
this._isText = selectionType === 'text';
this._isOle = selectionType === 'ole';
this._selection = undefined;
this._updateSelection();
this._selectedMode = 0;
},
empty: function () {
return !this._pointSet || this._pointSet.empty();
},
clear: function () {
this.setPointSet(new CPointSet());
},
setPointSet: function(pointSet) {
this._pointSet = pointSet;
this._updateSelection();
},
contains: function(corePxPoint) {
if (!this._selection)
return false;
return this._selection.anyRingBoundContains(corePxPoint);
},
getBounds: function() {
return this._selection.getBounds();
},
_updateSelection: function() {
if (!this._selection) {
if (!this._isOle) {
var fillColor = this._isView ?
L.LOUtil.rgbToHex(this._map.getViewColor(this._viewId)) :
this._styleData.getPropValue('background-color');
var opacity = this._styleData.getFloatPropValue('opacity');
var weight = this._styleData.getFloatPropWithoutUnit('border-top-width');
var attributes = this._isText ? {
viewId: this._isView ? this._viewId : undefined,
groupType: PathGroupType.TextSelection,
name: this._name,
pointerEvents: 'none',
fillColor: fillColor,
fillOpacity: opacity,
color: fillColor,
opacity: 0.60,
stroke: true,
fill: true,
weight: 1.0
} : {
viewId: this._isView ? this._viewId : undefined,
name: this._name,
pointerEvents: 'none',
color: fillColor,
fillColor: fillColor,
fillOpacity: opacity,
opacity: 1.0,
weight: Math.round(weight * app.dpiScale)
};
}
else {
var attributes = {
pointerEvents: 'none',
fillColor: 'black',
fillOpacity: 0.25,
weight: 0,
opacity: 0.25
};
}
if (this._isText) {
this._selection = new CPolygon(this._pointSet, attributes);
}
else if (this._isOle) {
this._selection = new CDarkOverlay(this._pointSet, attributes);
}
else {
this._selection = new CCellSelection(this._pointSet, attributes);
}
if (this._isText)
this._overlay.initPath(this._selection);
else
this._overlay.initPathGroup(this._selection);
return;
}
this._selection.setPointSet(this._pointSet);
},
remove: function() {
if (!this._selection)
return;
if (this._isText)
this._overlay.removePath(this._selection);
else
this._overlay.removePathGroup(this._selection);
},
});
// CReferences is used to store and manage the CPath's of all
// references in the current sheet.
var CReferences = L.Class.extend({
initialize: function (canvasOverlay) {
this._overlay = canvasOverlay;
this._marks = [];
},
// mark should be a CPath.
addMark: function (mark) {
this._overlay.initPath(mark);
this._marks.push(mark);
},
// mark should be a CPath.
hasMark: function (mark) {
for (var i = 0; i < this._marks.length; ++i) {
if (mark.getBounds().equals(this._marks[i].getBounds()))
return true;
}
return false;
},
clear: function () {
for (var i = 0; i < this._marks.length; ++i)
this._overlay.removePath(this._marks[i]);
this._marks = [];
}
});
L.TileCoordData = L.Class.extend({
initialize: function (left, top, zoom, part, mode) {
this.x = left;
this.y = top;
this.z = zoom;
this.part = part;
this.mode = (mode !== undefined) ? mode : 0;
},
getPos: function () {
return new L.Point(this.x, this.y);
},
key: function () {
return this.x + ':' + this.y + ':' + this.z + ':' + this.part + ':'
+ ((this.mode !== undefined) ? this.mode : 0);
},
toString: function () {
return '{ left : ' + this.x + ', top : ' + this.y +
', z : ' + this.z + ', part : ' + this.part + ', mode : ' + this.mode + ' }';
}
});
L.TileCoordData.parseKey = function (keyString) {
window.app.console.assert(typeof keyString === 'string', 'key should be a string');
var k = keyString.split(':');
var mode = (k.length === 4) ? +k[4] : 0;
window.app.console.assert(k.length >= 5, 'invalid key format');
return new L.TileCoordData(+k[0], +k[1], +k[2], +k[3], mode);
};
L.TileSectionManager = L.Class.extend({
initialize: function (layer) {
this._layer = layer;
this._canvas = this._layer._canvas;
this._map = this._layer._map;
var mapSize = this._map.getPixelBoundsCore().getSize();
this._tilesSection = null; // Shortcut.
this._sectionContainer = new CanvasSectionContainer(this._canvas, this._layer.isCalc() /* disableDrawing? */);
app.sectionContainer = this._sectionContainer;
if (L.Browser.cypressTest) // If cypress is active, create test divs.
this._sectionContainer.testing = true;
this._sectionContainer.onResize(mapSize.x, mapSize.y);
var splitPanesContext = this._layer.getSplitPanesContext();
this._splitPos = splitPanesContext ?
splitPanesContext.getSplitPos() : new L.Point(0, 0);
this._updatesRunning = false;
this._mirrorEventsFromSourceToCanvasSectionContainer(document.getElementById('map'));
var canvasContainer = document.getElementById('document-container');
var that = this;
this.resObserver = new ResizeObserver(function() {
that._layer._syncTileContainerSize();
});
this.resObserver.observe(canvasContainer);
this._zoomAtDocEdgeX = true;
this._zoomAtDocEdgeY = true;
},
// Map and TilesSection overlap entirely. Map is above tiles section. In order to handle events in tiles section, we need to mirror them from map.
_mirrorEventsFromSourceToCanvasSectionContainer: function (sourceElement) {
var that = this;
sourceElement.addEventListener('mousedown', function (e) { that._sectionContainer.onMouseDown(e); }, true);
sourceElement.addEventListener('click', function (e) { that._sectionContainer.onClick(e); }, true);
sourceElement.addEventListener('dblclick', function (e) { that._sectionContainer.onDoubleClick(e); }, true);
sourceElement.addEventListener('contextmenu', function (e) { that._sectionContainer.onContextMenu(e); }, true);
sourceElement.addEventListener('wheel', function (e) { that._sectionContainer.onMouseWheel(e); }, true);
sourceElement.addEventListener('mouseleave', function (e) { that._sectionContainer.onMouseLeave(e); }, true);
sourceElement.addEventListener('mouseenter', function (e) { that._sectionContainer.onMouseEnter(e); }, true);
sourceElement.addEventListener('touchstart', function (e) { that._sectionContainer.onTouchStart(e); }, true);
sourceElement.addEventListener('touchmove', function (e) { that._sectionContainer.onTouchMove(e); }, true);
sourceElement.addEventListener('touchend', function (e) { that._sectionContainer.onTouchEnd(e); }, true);
sourceElement.addEventListener('touchcancel', function (e) { that._sectionContainer.onTouchCancel(e); }, true);
},
startUpdates: function () {
if (this._updatesRunning === true) {
return false;
}
this._updatesRunning = true;
this._updateWithRAF();
return true;
},
stopUpdates: function () {
if (this._updatesRunning) {
L.Util.cancelAnimFrame(this._canvasRAF);
this.update();
this._updatesRunning = false;
return true;
}
return false;
},
dispose: function () {
this.stopUpdates();
},
getSplitPos: function () {
var splitPanesContext = this._layer.getSplitPanesContext();
return splitPanesContext ?
splitPanesContext.getSplitPos().multiplyBy(app.dpiScale) :
new L.Point(0, 0);
},
// Details of tile areas to render
_paintContext: function() {
var tileSize = new L.Point(this._layer._getTileSize(), this._layer._getTileSize());
var viewBounds = this._map.getPixelBoundsCore();
var splitPanesContext = this._layer.getSplitPanesContext();
var paneBoundsList = splitPanesContext ?
splitPanesContext.getPxBoundList(viewBounds) :
[viewBounds];
var canvasCorePx = new L.Point(this._pixWidth, this._pixHeight);
return { canvasSize: canvasCorePx,
tileSize: tileSize,
viewBounds: viewBounds,
paneBoundsList: paneBoundsList,
paneBoundsActive: splitPanesContext ? true: false,
splitPos: this.getSplitPos(),
};
},
coordsIntersectVisible: function (coords) {
if (!app.file.fileBasedView) {
var ctx = this._paintContext();
var tileBounds = new L.Bounds(new L.Point(coords.x, coords.y), new L.Point(coords.x + ctx.tileSize.x, coords.y + ctx.tileSize.y));
return tileBounds.intersectsAny(ctx.paneBoundsList);
}
else {
var ratio = this._layer._tileSize / this._layer._tileHeightTwips;
var partHeightPixels = Math.round((this._layer._partHeightTwips + this._layer._spaceBetweenParts) * ratio);
return L.LOUtil._doRectanglesIntersect(app.file.viewedRectangle.pToArray(), [coords.x, coords.y + partHeightPixels * coords.part, app.tile.size.pixels[0], app.tile.size.pixels[1]]);
}
},
_addTilesSection: function () {
this._sectionContainer.addSection(L.getNewTilesSection());
this._tilesSection = this._sectionContainer.getSectionWithName('tiles');
app.sectionContainer.setDocumentAnchorSection(L.CSections.Tiles.name);
},
_addGridSection: function () {
var that = this;
this._sectionContainer.createSection({
name: L.CSections.CalcGrid.name,
anchor: 'top left',
position: [0, 0],
size: [0, 0],
expand: '',
processingOrder: L.CSections.CalcGrid.processingOrder, // Size and position will be copied, this value is not important.
drawingOrder: L.CSections.CalcGrid.drawingOrder,
zIndex: L.CSections.CalcGrid.zIndex,
// Even if this one is drawn on top, won't be able to catch events.
// Sections with "interactable: true" can catch events even if they are under a section with property "interactable: false".
interactable: false,
sectionProperties: {
docLayer: that._layer,
tsManager: that,
strokeStyle: '#c0c0c0'
},
onDraw: that._onDrawGridSection,
onDrawArea: that._drawGridSectionArea
}, 'tiles'); // Its size and position will be copied from 'tiles' section.
this._calcGridSection = this._sectionContainer.getSectionWithName(L.CSections.CalcGrid.name);
},
_addOverlaySection: function () {
var canvasOverlay = this._layer._canvasOverlay = new CanvasOverlay(this._map, this._sectionContainer.getContext());
this._sectionContainer.addSection(canvasOverlay);
canvasOverlay.bindToSection(L.CSections.Tiles.name);
},
_onDrawGridSection: function () {
if (this.containerObject.isInZoomAnimation() || this.sectionProperties.tsManager.waitForTiles())
return;
// We don't show the sheet grid, so we don't draw it.
if (!this.sectionProperties.docLayer._sheetGrid)
return;
// grid-section's onDrawArea is TileSectionManager's _drawGridSectionArea().
this.onDrawArea();
},
_drawGridSectionArea: function (repaintArea, paneTopLeft, canvasCtx) {
if (!this.sectionProperties.docLayer.sheetGeometry)
return;
var context = canvasCtx ? canvasCtx : this.context;
var tsManager = this.sectionProperties.tsManager;
context.strokeStyle = this.sectionProperties.strokeStyle;
context.lineWidth = 1.0;
var scale = 1.0;
if (tsManager._inZoomAnim && tsManager._zoomFrameScale)
scale = tsManager._zoomFrameScale;
var ctx = this.sectionProperties.tsManager._paintContext();
var isRTL = this.sectionProperties.docLayer.isLayoutRTL();
var sectionWidth = this.size[0];
var xTransform = function (xcoord) {
return isRTL ? sectionWidth - xcoord : xcoord;
};
// This is called just before and after the dashed line drawing.
var startEndDash = function (ctx2D, end) {
// Style the dashed lines.
var dashLen = 5;
var gapLen = 5;
// Restart the path to apply the dashed line style.
ctx2D.closePath();
ctx2D.beginPath();
ctx2D.setLineDash(end ? [] : [dashLen, gapLen]);
};
var docLayer = this.sectionProperties.docLayer;
var currentPart = docLayer._selectedPart;
// Draw the print range with dashed line if singleton to match desktop Calc.
var printRange = [];
if (docLayer._printRanges && docLayer._printRanges.length > currentPart
&& docLayer._printRanges[currentPart].length == 1)
printRange = docLayer._printRanges[currentPart][0];
for (var i = 0; i < ctx.paneBoundsList.length; ++i) {
// co-ordinates of this pane in core document pixels
var paneBounds = ctx.paneBoundsList[i];
// co-ordinates of the main-(bottom right) pane in core document pixels
var viewBounds = ctx.viewBounds;
// into real pixel-land ...
paneBounds.round();
viewBounds.round();
var paneOffset;
var doOnePane = false;
if (!repaintArea || !paneTopLeft) {
repaintArea = paneBounds;
paneOffset = paneBounds.getTopLeft(); // allocates
// Cute way to detect the in-canvas pixel offset of each pane
paneOffset.x = Math.min(paneOffset.x, viewBounds.min.x);
paneOffset.y = Math.min(paneOffset.y, viewBounds.min.y);
} else {
// do only for the predefined pane (paneOffset / repaintArea)
doOnePane = true;
paneOffset = paneTopLeft.clone();
}
// Vertical line rendering on large areas is ~10x as expensive
// as horizontal line rendering: due to cache effects - so to
// help our poor CPU renderers - render in horizontal strips.
var bandSize = 256;
var clearDash = false;
for (var miny = repaintArea.min.y; miny < repaintArea.max.y; miny += bandSize)
{
var maxy = Math.min(repaintArea.max.y, miny + bandSize);
context.beginPath();
// vertical lines
this.sectionProperties.docLayer.sheetGeometry._columns.forEachInCorePixelRange(
repaintArea.min.x, repaintArea.max.x,
function(pos, colIndex) {
var xcoord = xTransform(Math.floor(scale * (pos - paneOffset.x)) - 0.5);
clearDash = false;
if (printRange.length === 4
&& (printRange[0] === colIndex || printRange[2] + 1 === colIndex)) {
clearDash = true;
startEndDash(context, false /* end? */);
}
context.moveTo(xcoord, Math.floor(scale * (miny - paneOffset.y)) + 0.5);
context.lineTo(xcoord, Math.floor(scale * (maxy - paneOffset.y)) - 0.5);
context.stroke();
if (clearDash)
startEndDash(context, true /* end? */);
});
// horizontal lines
this.sectionProperties.docLayer.sheetGeometry._rows.forEachInCorePixelRange(
miny, maxy,
function(pos, rowIndex) {
clearDash = false;
if (printRange.length === 4
&& (printRange[1] === rowIndex || printRange[3] + 1 === rowIndex)) {
clearDash = true;
startEndDash(context, false /* end? */);
}
context.moveTo(
xTransform(Math.floor(scale * (repaintArea.min.x - paneOffset.x)) + 0.5),
Math.floor(scale * (pos - paneOffset.y)) - 0.5);
context.lineTo(
xTransform(Math.floor(scale * (repaintArea.max.x - paneOffset.x)) - 0.5),
Math.floor(scale * (pos - paneOffset.y)) - 0.5);
context.stroke();
if (clearDash)
startEndDash(context, true /* end? */);
});
context.closePath();
}
if (doOnePane)
break;
}
},
// Debug tool. Splits are enabled for only Calc for now.
_addSplitsSection: function () {
var that = this;
this._sectionContainer.createSection({
name: L.CSections.Debug.Splits.name,
anchor: 'top left',
position: [0, 0],
size: [0, 0],
expand: '',
processingOrder: L.CSections.Debug.Splits.processingOrder,
drawingOrder: L.CSections.Debug.Splits.drawingOrder,
zIndex: L.CSections.Debug.Splits.zIndex,
// Even if this one is drawn on top, won't be able to catch events.
// Sections with "interactable: true" can catch events even if they are under a section with property "interactable: false".
interactable: false,
sectionProperties: {
docLayer: that._layer
},
onDraw: that._onDrawSplitsSection
}, 'tiles'); // Its size and position will be copied from 'tiles' section.
this._sectionContainer.reNewAllSections(true);
},
_removeSplitsSection: function () {
var section = this._sectionContainer.getSectionWithName('calc grid');
if (section) {
section.setDrawingOrder(L.CSections.CalcGrid.drawingOrder);
section.sectionProperties.strokeStyle = '#c0c0c0';
}
this._sectionContainer.removeSection(L.CSections.Debug.Splits.name);
this._sectionContainer.reNewAllSections(true);
},
// Debug tool
_addTilePixelGridSection: function () {
var that = this;
this._sectionContainer.createSection({
name: L.CSections.Debug.TilePixelGrid.name,
anchor: 'top left',
position: [0, 0],
size: [0, 0],
expand: '',
processingOrder: L.CSections.Debug.TilePixelGrid.processingOrder, // Size and position will be copied, this value is not important.
drawingOrder: L.CSections.Debug.TilePixelGrid.drawingOrder,
zIndex: L.CSections.Debug.TilePixelGrid.zIndex,
interactable: false,
sectionProperties: {},
onDraw: that._onDrawTilePixelGrid
}, 'tiles'); // Its size and position will be copied from 'tiles' section.
this._sectionContainer.reNewAllSections(true);
},
_removeTilePixelGridSection: function () {
this._sectionContainer.removeSection(L.CSections.Debug.TilePixelGrid.name);
this._sectionContainer.reNewAllSections(true);
},
_onDrawTilePixelGrid: function() {
var offset = 8;
var count;
this.context.lineWidth = 1;
var currentPos;
this.context.strokeStyle = '#ff0000';
currentPos = 0;
count = Math.round(this.context.canvas.height / offset);
for (var i = 0; i < count; i++) {
this.context.beginPath();
this.context.moveTo(0.5, currentPos + 0.5);
this.context.lineTo(this.context.canvas.width + 0.5, currentPos + 0.5);
this.context.stroke();
currentPos += offset;
}
currentPos = 0;
count = Math.round(this.context.canvas.width / offset);
for (var i = 0; i < count; i++) {
this.context.beginPath();
this.context.moveTo(currentPos + 0.5, 0.5);
this.context.lineTo(currentPos + 0.5, this.context.canvas.height + 0.5);
this.context.stroke();
currentPos += offset;
}
},
_onDrawSplitsSection: function () {
var splitPanesContext = this.sectionProperties.docLayer.getSplitPanesContext();
if (splitPanesContext) {
var splitPos = splitPanesContext.getSplitPos();
this.context.strokeStyle = 'red';
this.context.strokeRect(0, 0, splitPos.x * app.dpiScale, splitPos.y * app.dpiScale);
}
},
_updateWithRAF: function () {
// update-loop with requestAnimationFrame
this._canvasRAF = L.Util.requestAnimFrame(this._updateWithRAF, this, false /* immediate */);
this._sectionContainer.requestReDraw();
},
update: function () {
this._sectionContainer.requestReDraw();
},
/**
* Everything in this doc comment is speculation: I didn't write the code that supplies it and I'm guessing to
* have something to work on for this function. That said, given my observations, they seem incredibly likely to be correct
*
* @param pinchCenter {{x: number, y: number}} The current pinch center in doc core-pixels
* Normally expressed as an L.Point instance
*
* @param pinchStartCenter {{x: number, y: number}} The pinch center at the start of the pinch in doc core-pixels
* Normally expressed as an L.Point instance
*
* @param paneBounds {{min: {x: number, y: number}, max: {x: number, y: number}}} The edges of the current pane
* Traditionally this is the map border at the start of the pinch
*
* @param freezePane {{freezeX: boolean, freezeY: boolean}} Whether the pane is frozen in the x or y directions
*
* @param splitPos {{x: number, y: number}} The inset in core-pixels into the document caused by any splits (e.g. a frozen row at the start of the document)
*
* @param scale {number} The scale, relative to the initial size, of the document currently
* Or rather this is equivalent to: old_width / new_width
*
* @param findFreePaneCenter {boolean} Wether to return a center point
*
* @returns {{topLeft: {x: number, y: number}, center?: {x: number, y: number}}} An object with a top left point in core-pixels and optionally a center point
* Center is included iff findFreePaneCenter is true
* (probably this should be encoded into the type, e.g. with an overload when this is converted to TypeScript)
**/
_getZoomDocPos: function (pinchCenter, pinchStartCenter, paneBounds, freezePane, splitPos, scale, findFreePaneCenter) {
let xMin = 0;
const hasXMargin = !this._layer.isCalc();
if (hasXMargin) {
xMin = -Infinity;
} else if (paneBounds.min.x > 0) {
xMin = splitPos.x;
}
let yMin = 0;
if (paneBounds.min.y < 0) {
yMin = -Infinity;
} else if (paneBounds.min.y > 0) {
yMin = splitPos.y;
}
const documentTopLeft = new L.Point(xMin, yMin);
const paneSize = paneBounds.getSize();
let centerOffset = {
x: pinchCenter.x - pinchStartCenter.x,
y: pinchCenter.y - pinchStartCenter.y,
};
// Portion of the pane away that our pinchStart (which should be where we zoom round) is
const panePortion = {
x: (pinchStartCenter.x - paneBounds.min.x) / paneSize.x,
y: (pinchStartCenter.y - paneBounds.min.y) / paneSize.y,
};
// Top left in document coordinates.
const docTopLeft = new L.Point(
Math.max(documentTopLeft.x, pinchStartCenter.x + (centerOffset.x - paneSize.x * panePortion.x) / scale),
Math.max(documentTopLeft.y, pinchStartCenter.y + (centerOffset.y - paneSize.y * panePortion.y) / scale)
);
if (freezePane.freezeX) {
docTopLeft.x = paneBounds.min.x;
}
if (freezePane.freezeY) {
docTopLeft.y = paneBounds.min.y;
}
if (!findFreePaneCenter) {
return { topLeft: docTopLeft };
}
const newPaneCenter = new L.Point(
(docTopLeft.x - splitPos.x + (paneSize.x + splitPos.x) * 0.5 / scale) / app.dpiScale,
(docTopLeft.y - splitPos.y + (paneSize.y + splitPos.y) * 0.5 / scale) / app.dpiScale);
return {
topLeft: docTopLeft,
center: this._map.project(this._map.unproject(newPaneCenter, this._map.getZoom()), this._map.getScaleZoom(scale))
};
},
_getZoomMapCenter: function (zoom) {
var scale = this._calcZoomFrameScale(zoom);
var ctx = this._paintContext();
var splitPos = ctx.splitPos;
var viewBounds = ctx.viewBounds;
var freePaneBounds = new L.Bounds(viewBounds.min.add(splitPos), viewBounds.max);
return this._getZoomDocPos(
this._newCenter,
this._layer._pinchStartCenter,
freePaneBounds,
{ freezeX: false, freezeY: false },
splitPos,
scale,
true /* findFreePaneCenter */
).center;
},
_zoomAnimation: function () {
var painter = this;
var ctx = this._paintContext();
var canvasOverlay = this._layer._canvasOverlay;
var rafFunc = function (timeStamp, final) {
// Draw zoom frame with grids and directly from the tiles.
// This will clear the doc area first.
painter._tilesSection.drawZoomFrame(ctx);
// Draw the overlay objects.
canvasOverlay.onDraw();
if (!final)
painter._zoomRAF = requestAnimationFrame(rafFunc);
};
this.rafFunc = rafFunc;
rafFunc();
},
_calcZoomFrameScale: function (zoom) {
zoom = this._layer._map._limitZoom(zoom);
var origZoom = this._layer._map.getZoom();
// Compute relative-multiplicative scale of this zoom-frame w.r.t the starting zoom(ie the current Map's zoom).
return this._layer._map.zoomToFactor(zoom - origZoom + this._layer._map.options.zoom);
},
_calcZoomFrameParams: function (zoom, newCenter) {
this._zoomFrameScale = this._calcZoomFrameScale(zoom);
this._newCenter = this._layer._map.project(newCenter).multiplyBy(app.dpiScale); // in core pixels
},
setWaitForTiles: function (wait) {
this._waitForTiles = wait;
},
waitForTiles: function () {
return this._waitForTiles;
},
zoomStep: function (zoom, newCenter) {
if (this._finishingZoom) // finishing steps of animation still going on.
return;
this._calcZoomFrameParams(zoom, newCenter);
if (!this._inZoomAnim) {
this._sectionContainer.setInZoomAnimation(true);
this._inZoomAnim = true;
// Start RAF loop for zoom-animation
this._zoomAnimation();
}
},
zoomStepEnd: function (zoom, newCenter, mapUpdater, runAtFinish, noGap) {
if (!this._inZoomAnim || this._finishingZoom)
return;
this._finishingZoom = true;
this._map.disableTextInput();
// Do a another animation from current non-integral log-zoom to
// the final integral zoom, but maintain the same center.
var steps = 10;
var stepId = noGap ? steps : 0;
var startZoom = this._zoomFrameScale;
var endZoom = this._calcZoomFrameScale(zoom);
var painter = this;
var map = this._map;
// Calculate the final center at final zoom in advance.
var newMapCenter = this._getZoomMapCenter(zoom);
var newMapCenterLatLng = map.unproject(newMapCenter, zoom);
painter._sectionContainer.setZoomChanged(true);
var stopAnimation = noGap ? true : false;
var waitForTiles = false;
var waitTries = 30;
var finishingRAF = undefined;
var finishAnimation = function () {
if (stepId < steps) {
// continue animating till we reach "close" to 'final zoom'.
painter._zoomFrameScale = startZoom + (endZoom - startZoom) * stepId / steps;
stepId += 1;
if (stepId >= steps)
stopAnimation = true;
}
if (stopAnimation) {
stopAnimation = false;
cancelAnimationFrame(painter._zoomRAF);
painter._calcZoomFrameParams(zoom, newCenter);
// Draw one last frame at final zoom.
painter.rafFunc(undefined, true /* final? */);
painter._zoomFrameScale = undefined;
painter._sectionContainer.setInZoomAnimation(false);
painter._inZoomAnim = false;
painter.setWaitForTiles(true);
// Set view and paint the tiles if all available.
mapUpdater(newMapCenterLatLng);
waitForTiles = true;
}
if (waitForTiles) {
// Wait until we get all tiles or wait time exceeded.
if (waitTries <= 0 || painter._tilesSection.haveAllTilesInView()) {
// All done.
waitForTiles = false;
cancelAnimationFrame(finishingRAF);
painter.setWaitForTiles(false);
painter._sectionContainer.setZoomChanged(false);
map.enableTextInput();
map.focus(map.canAcceptKeyboardInput());
// Paint everything.
painter._sectionContainer.requestReDraw();
// Don't let a subsequent pinchZoom start before finishing all steps till this point.
painter._finishingZoom = false;
// Run the finish callback.
runAtFinish();
return;
}
else
waitTries -= 1;
}
finishingRAF = requestAnimationFrame(finishAnimation);
};
finishAnimation();
},
getTileSectionPos : function () {
return new L.Point(this._tilesSection.myTopLeft[0], this._tilesSection.myTopLeft[1]);
}
});
L.CanvasTileLayer = L.Layer.extend({
options: {
pane: 'tilePane',
tileSize: window.tileSize,
opacity: 1,
updateWhenIdle: (window.mode.isMobile() || window.mode.isTablet()),
updateInterval: 200,
attribution: null,
zIndex: null,
bounds: null,
previewInvalidationTimeout: 1000,
},
_pngCache: [],
initialize: function (options) {
options = L.setOptions(this, options);
this._tileWidthPx = options.tileSize;
this._tileHeightPx = options.tileSize;
// text, presentation, spreadsheet, etc
this._docType = options.docType;
this._documentInfo = '';
app.file.textCursor.visible = false;
// Last cursor position for invalidation
this.lastCursorPos = null;
// Are we zooming currently ? - if so, no cursor.
this._isZooming = false;
// Original rectangle graphic selection in twips
this._graphicSelectionTwips = new L.Bounds(new L.Point(0, 0), new L.Point(0, 0));
// Rectangle graphic selection
this._graphicSelection = new L.LatLngBounds(new L.LatLng(0, 0), new L.LatLng(0, 0));
// Rotation angle of selected graphic object
this._graphicSelectionAngle = 0;
app.calc.cellCursorVisible = false;
this._prevCellCursor = null;
this._prevCellCursorAddress = null;
this._cellCursorOnPgUp = null;
this._cellCursorOnPgDn = null;
this._shapeGridOffset = new L.Point(0, 0);
// Tile garbage collection counter
this._gcCounter = 0;
// Queue of tiles which were GC'd earlier than coolwsd expected
this._fetchKeyframeQueue = [];
// Position and size of the selection start (as if there would be a cursor caret there).
// View cursors with viewId to 'cursor info' mapping
// Eg: 1: {rectangle: 'x, y, w, h', visible: false}
this._viewCursors = {};
// View cell cursors with viewId to 'cursor info' mapping.
this._cellViewCursors = {};
// View selection of other views
this._viewSelections = {};
// Graphic view selection rectangles
this._graphicViewMarkers = {};
this._lastValidPart = -1;
// Cursor marker
this._cursorMarker = null;
// Graphic marker
this._graphicMarker = null;
// Graphic Selected?
this._hasActiveSelection = false;
// Selection handle marker
this._selectionHandles = {};
['start', 'end'].forEach(L.bind(function (handle) {
this._selectionHandles[handle] = L.marker(new L.LatLng(0, 0), {
icon: L.divIcon({
className: 'leaflet-selection-marker-' + handle,
iconSize: null
}),
draggable: true
});
}, this));
this._cellResizeMarkerStart = L.marker(new L.LatLng(0, 0), {
icon: L.divIcon({
className: 'spreadsheet-cell-resize-marker',
iconSize: null
}),
draggable: true
});
this._cellResizeMarkerEnd = L.marker(new L.LatLng(0, 0), {
icon: L.divIcon({
className: 'spreadsheet-cell-resize-marker',
iconSize: null
}),
draggable: true
});
this._referenceMarkerStart = L.marker(new L.LatLng(0, 0), {
icon: L.divIcon({
className: 'spreadsheet-cell-resize-marker',
iconSize: null
}),
draggable: true
});
this._referenceMarkerEnd = L.marker(new L.LatLng(0, 0), {
icon: L.divIcon({
className: 'spreadsheet-cell-resize-marker',
iconSize: null
}),
draggable: true
});
this._initializeTableOverlay();
this._emptyTilesCount = 0;
this._msgQueue = [];
this._toolbarCommandValues = {};
this._previewInvalidations = [];
this._followThis = -1;
this._editorId = -1;
this._followUser = false;
this._followEditor = false;
this._selectedTextContent = '';
this._typingMention = false;
this._mentionText = [];
this._moveInProgress = false;
this._canonicalIdInitialized = false;
this._nullDeltaUpdate = 0;
},
_initContainer: function () {
if (this._canvasContainer) {
window.app.console.error('called _initContainer() when this._canvasContainer is present!');
}
if (this._container) { return; }
this._container = L.DomUtil.create('div', 'leaflet-layer');
this._updateZIndex();
if (this.options.opacity < 1) {
this._updateOpacity();
}
this.getPane().appendChild(this._container);
var mapContainer = document.getElementById('document-container');
var canvasContainerClass = 'leaflet-canvas-container';
this._canvasContainer = L.DomUtil.create('div', canvasContainerClass, mapContainer);
this._canvasContainer.id = 'canvas-container';
this._setup();
},
_setup: function () {
if (!this._canvasContainer) {
window.app.console.error('canvas container not found. _initContainer failed ?');
}
this._canvas = L.DomUtil.createWithId('canvas', 'document-canvas', this._canvasContainer);
this._container.style.position = 'absolute';
this._cursorDataDiv = L.DomUtil.create('div', 'cell-cursor-data', this._canvasContainer);
this._selectionsDataDiv = L.DomUtil.create('div', 'selections-data', this._canvasContainer);
this._splittersDataDiv = L.DomUtil.create('div', 'splitters-data', this._canvasContainer);
this._cursorOverlayDiv = L.DomUtil.create('div', 'cursor-overlay', this._canvasContainer);
if (L.Browser.cypressTest) {
this._emptyDeltaDiv = L.DomUtil.create('div', 'empty-deltas', this._canvasContainer);
this._emptyDeltaDiv.innerText = 0;
}
this._splittersStyleData = new CStyleData(this._splittersDataDiv);
this._painter = new L.TileSectionManager(this);
this._painter._addTilesSection();
this._painter._sectionContainer.getSectionWithName('tiles').onResize();
this._painter._addOverlaySection();
this._painter._sectionContainer.addSection(L.getNewScrollSection(() => this._map._docLayer.isCalcRTL()));
// For mobile/tablet the hammerjs swipe handler already uses a requestAnimationFrame to fire move/drag events
// Using L.TileSectionManager's own requestAnimationFrame loop to do the updates in that case does not perform well.
if (window.mode.isMobile() || window.mode.isTablet()) {
this._map.on('move', this._painter.update, this._painter);
this._map.on('moveend', function () {
setTimeout(this.update.bind(this), 200);
}, this._painter);
}
else if (this._docType !== 'spreadsheet') { // See scroll section. panBy is used for spreadsheets while scrolling.
this._map.on('movestart', this._painter.startUpdates, this._painter);
this._map.on('moveend', this._painter.stopUpdates, this._painter);
}
this._map.on('resize', this._syncTileContainerSize, this);
this._map.on('zoomend', this._painter.update, this._painter);
this._map.on('splitposchanged', this._painter.update, this._painter);
this._map.on('sheetgeometrychanged', this._painter.update, this._painter);
this._map.on('move', this._syncTilePanePos, this);
this._map.on('viewrowcolumnheaders', this._painter.update, this._painter);
this._map.on('messagesdone', this._sendProcessedResponse, this);
this._queuedProcessed = [];
if (this._docType === 'spreadsheet') {
this._painter._addGridSection();
}
// Add it regardless of the file type.
app.sectionContainer.addSection(new app.definitions.CommentSection());
this._syncTileContainerSize();
this._setupTableOverlay();
},
// Returns true if the document type is Writer.
isWriter: function() {
return this._docType === 'text';
},
// Returns true if the document type is Calc.
isCalc: function() {
return this._docType === 'spreadsheet';
},
// Returns true if the document type is Impress.
isImpress: function() {
return this._docType === 'presentation';
},
bringToFront: function () {
if (this._map) {
L.DomUtil.toFront(this._container);
this._setAutoZIndex(Math.max);
}
return this;
},
bringToBack: function () {
if (this._map) {
L.DomUtil.toBack(this._container);
this._setAutoZIndex(Math.min);
}
return this;
},
getAttribution: function () {
return this.options.attribution;
},
getContainer: function () {
return this._container;
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
if (this._map) {
this._updateOpacity();
}
return this;
},
setZIndex: function (zIndex) {
this.options.zIndex = zIndex;
this._updateZIndex();
return this;
},
redraw: function () {
if (this._map) {
this._removeAllTiles();
this._update();
}
return this;
},
_updateZIndex: function () {
if (this._container && this.options.zIndex !== undefined && this.options.zIndex !== null) {
this._container.style.zIndex = this.options.zIndex;
}
},
_setAutoZIndex: function (compare) {
// go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)
var layers = this.getPane().children,
edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min
for (var i = 0, len = layers.length, zIndex; i < len; i++) {
zIndex = layers[i].style.zIndex;
if (layers[i] !== this._container && zIndex) {
edgeZIndex = compare(edgeZIndex, +zIndex);
}
}
if (isFinite(edgeZIndex)) {
this.options.zIndex = edgeZIndex + compare(-1, 1);
this._updateZIndex();
}
},
_removeAllTiles: function () {
for (var key in this._tiles) {
this._removeTile(key);
}
},
_reset: function (hard) {
var tileZoom = Math.round(this._map.getZoom()),
tileZoomChanged = this._tileZoom !== tileZoom;
this._tileSize = this._getTileSize();
if (hard || tileZoomChanged) {
this._resetClientVisArea();
this._tileZoom = tileZoom;
if (tileZoomChanged) {
this._updateTileTwips();
this._updateMaxBounds();
}
app.tile.size.pixels = [this._tileSize, this._tileSize];
if (this._tileWidthTwips === undefined) {
this._tileWidthTwips = this.options.tileWidthTwips;
app.tile.size.twips[0] = this.options.tileWidthTwips;
}
if (this._tileHeightTwips === undefined) {
this._tileHeightTwips = this.options.tileHeightTwips;
app.tile.size.twips[1] = this.options.tileHeightTwips;
}
app.twipsToPixels = app.tile.size.pixels[0] / app.tile.size.twips[0];
app.pixelsToTwips = app.tile.size.twips[0] / app.tile.size.pixels[0];
if (!L.Browser.mobileWebkit)
this._update(this._map.getCenter(), tileZoom);
this._pruneTiles();
}
},
// These variables indicates the clientvisiblearea sent to the server and stored by the server
// We need to reset them when we are reconnecting to the server or reloading a document
// because the server needs new data even if the client is unmodified.
_resetClientVisArea: function () {
this._clientZoom = '';
this._clientVisibleArea = '';
},
_resetCanonicalIdStatus: function() {
this._canonicalIdInitialized = false;
},
_resetViewId: function () {
this._viewId = undefined;
},
_getViewId: function () {
return this._viewId;
},
_updateTileTwips: function () {
// smaller zoom = zoom in
var factor = Math.pow(1.2, (this._map.options.zoom - this._tileZoom));
this._tileWidthTwips = Math.round(this.options.tileWidthTwips * factor);
this._tileHeightTwips = Math.round(this.options.tileHeightTwips * factor);
app.tile.size.twips = [this._tileWidthTwips, this._tileHeightTwips];
app.file.size.pixels = [Math.round(app.tile.size.pixels[0] * (app.file.size.twips[0] / app.tile.size.twips[0])), Math.round(app.tile.size.pixels[1] * (app.file.size.twips[1] / app.tile.size.twips[1]))];
app.view.size.pixels = app.file.size.pixels.slice();
app.twipsToPixels = app.tile.size.pixels[0] / app.tile.size.twips[0];
app.pixelsToTwips = app.tile.size.twips[0] / app.tile.size.pixels[0];
},
_checkSpreadSheetBounds: function (newZoom) {
// for spreadsheets, when the document is smaller than the viewing area
// we want it to be glued to the row/column headers instead of being centered
// In the future we probably want to remove this and set the bonds only on the
// left/upper side of the spreadsheet so that we can have an 'infinite' number of
// cells downwards and to the right, like we have on desktop
var viewSize = this._map.getSize();
var scale = this._map.getZoomScale(newZoom);
var width = this._docWidthTwips / this._tileWidthTwips * this._tileSize * scale;
var height = this._docHeightTwips / this._tileHeightTwips * this._tileSize * scale;
if (width < viewSize.x || height < viewSize.y) {
// if after zoomimg the document becomes smaller than the viewing area
width = Math.max(width, viewSize.x);
height = Math.max(height, viewSize.y);
if (!this._map.options._origMaxBounds) {
this._map.options._origMaxBounds = this._map.options.maxBounds;
}
scale = this._map.options.crs.scale(1);
this._map.setMaxBounds(new L.LatLngBounds(
this._map.unproject(new L.Point(0, 0)),
this._map.unproject(new L.Point(width * scale, height * scale))));
}
else if (this._map.options._origMaxBounds) {
// if after zoomimg the document becomes larger than the viewing area
// we need to restore the initial bounds
this._map.setMaxBounds(this._map.options._origMaxBounds);
this._map.options._origMaxBounds = null;
}
},
_updateScrollOffset: function () {
if (!this._map) return;
var centerPixel = this._map.project(this._map.getCenter());
var newScrollPos = centerPixel.subtract(this._map.getSize().divideBy(2));
var x = Math.round(newScrollPos.x < 0 ? 0 : newScrollPos.x);
var y = Math.round(newScrollPos.y < 0 ? 0 : newScrollPos.y);
this._map.fire('updatescrolloffset', {x: x, y: y, updateHeaders: true});
},
_getTileSize: function () {
return this.options.tileSize;
},
_moveStart: function () {
this._resetPreFetching();
this._moveInProgress = true;
},
_move: function () {
// We throttle the "move" event, but in moveEnd we always call
// a _move anyway, so if there are throttled moves still
// pending by the time moveEnd is called then there is no point
// processing them after _moveEnd because we are up to date
// already when they arrive and to do would just duplicate tile
// requests
if (!this._moveInProgress)
return;
this._update();
this._resetPreFetching(true);
this._onCurrentPageUpdate();
},
_moveEnd: function () {
this._move();
this._moveInProgress = false;
},
_requestNewTiles: function () {
this._onMessage('invalidatetiles: EMPTY', null);
this._update();
},
_refreshTilesInBackground: function() {
for (var key in this._tiles) {
this._tiles[key].wireId = 0;
this._tiles[key].invalidFrom = 0;
}
},
_sendClientZoom: function (forceUpdate) {
if (!this._map._docLoaded)
return;
var newClientZoom = 'tilepixelwidth=' + this._tileWidthPx + ' ' +
'tilepixelheight=' + this._tileHeightPx + ' ' +
'tiletwipwidth=' + this._tileWidthTwips + ' ' +
'tiletwipheight=' + this._tileHeightTwips;
if (this._clientZoom !== newClientZoom || forceUpdate) {
// the zoom level has changed
app.socket.sendMessage('clientzoom ' + newClientZoom);
if (!this._map._fatal && app.idleHandler._active && app.socket.connected())
this._clientZoom = newClientZoom;
}
},
_twipsRectangleToPixelBounds: function (strRectangle) {
// TODO use this more
// strRectangle = x, y, width, height
var strTwips = strRectangle.match(/\d+/g);
if (!strTwips) {
return null;
}
var topLeftTwips = new L.Point(parseInt(strTwips[0]), parseInt(strTwips[1]));
var offset = new L.Point(parseInt(strTwips[2]), parseInt(strTwips[3]));
var bottomRightTwips = topLeftTwips.add(offset);
return new L.Bounds(
this._twipsToPixels(topLeftTwips),
this._twipsToPixels(bottomRightTwips));
},
_twipsRectanglesToPixelBounds: function (strRectangles) {
// used when we have more rectangles
strRectangles = strRectangles.split(';');
var boundsList = [];
for (var i = 0; i < strRectangles.length; i++) {
var bounds = this._twipsRectangleToPixelBounds(strRectangles[i]);
if (bounds) {
boundsList.push(bounds);
}
}
return boundsList;
},
_initPreFetchPartTiles: function() {
// check existing timeout and clear it before the new one
if (this._partTilePreFetcher)
clearTimeout(this._partTilePreFetcher);
this._partTilePreFetcher =
setTimeout(
L.bind(function() {
this._preFetchPartTiles(this._selectedPart + this._map._partsDirection, this._selectedMode);
},
this),
100 /*ms*/);
},
_preFetchPartTiles: function(part, mode) {
var center = this._map.getCenter();
var zoom = this._map.getZoom();
var pixelBounds = this._map.getPixelBoundsCore(center, zoom);
var tileRange = this._pxBoundsToTileRange(pixelBounds);
var tileCombineQueue = [];
for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
var coords = new L.TileCoordData(i * this._tileSize, j * this._tileSize, zoom, part, mode);
if (!this._isValidTile(coords))
continue;
var key = this._tileCoordsToKey(coords);
if (!this._tileNeedsFetch(key))
continue;
tileCombineQueue.push(coords);
}
}
this._sendTileCombineRequest(tileCombineQueue);
},
_sendTileCombineRequest: function(tileCombineQueue) {
if (tileCombineQueue.length <= 0)
return;
// Sort into buckets of consistent part & mode.
var partMode = {};
for (var i = 0; i < tileCombineQueue.length; ++i)
{
var coords = tileCombineQueue[i];
// mode is a small number - give it 8 bits
var pmKey = (coords.part << 8) + coords.mode;
if (partMode[pmKey] === undefined)
partMode[pmKey] = [];
partMode[pmKey].push(coords);
}
for (var pmKey in partMode) {
// no keys method
var partTileQueue = partMode[pmKey];
var part = partTileQueue[0].part;
var mode = partTileQueue[0].mode;
var tilePositionsX = [];
var tilePositionsY = [];
var tileWids = [];
var added = {}; // uniqify
for (var i = 0; i < partTileQueue.length; ++i)
{
var coords = partTileQueue[i];
var key = this._tileCoordsToKey(coords);
// request each tile just once in these tilecombines
if (added[key])
continue;
added[key] = true;
// build parameters
var tile = this._tiles[key];
tileWids.push((tile && tile.wireId !== undefined) ? tile.wireId : 0);
var twips = this._coordsToTwips(coords);
tilePositionsX.push(twips.x);
tilePositionsY.push(twips.y);
}
var msg = 'tilecombine ' +
'nviewid=0 ' +
'part=' + part + ' ' +
((mode !== 0) ? ('mode=' + mode + ' ') : '') +
'width=' + this._tileWidthPx + ' ' +
'height=' + this._tileHeightPx + ' ' +
'tileposx=' + tilePositionsX.join(',') + ' ' +
'tileposy=' + tilePositionsY.join(',') + ' ' +
'oldwid=' + tileWids.join(',') + ' ' +
'tilewidth=' + this._tileWidthTwips + ' ' +
'tileheight=' + this._tileHeightTwips;
app.socket.sendMessage(msg, '');
}
},
getMaxDocSize: function () {
return undefined;
},
getSnapDocPosX: function (docPosPixX) {
return docPosPixX;
},
getSnapDocPosY: function (docPosPixY) {
return docPosPixY;
},
getSplitPanesContext: function () {
return undefined;
},
_createNewMouseEvent: function (type, inputEvent) {
var event = inputEvent;
if (inputEvent.type == 'touchstart' || inputEvent.type == 'touchmove') {
event = inputEvent.touches[0];
}
else if (inputEvent.type == 'touchend') {
event = inputEvent.changedTouches[0];
}
var newEvent = document.createEvent('MouseEvents');
newEvent.initMouseEvent(
type, true, true, window, 1,
event.screenX, event.screenY,
event.clientX, event.clientY,
false, false, false, false, 0, null
);
return newEvent;
},
createTile: function (coords, key) {
if (this._tiles[key])
{
if (this._debugDeltas)
window.app.console.debug('Already created tile ' + key);
return this._tiles[key];
}
var tile = {
coords: coords,
current: true, // is this currently visible
canvas: null, // canvas ready to render
imgDataCache: null, // flat byte array of canvas data
rawDeltas: null, // deltas ready to decompress
deltaCount: 0, // how many deltas on top of the keyframe
updateCount: 0, // how many updates did we have
loadCount: 0, // how many times did we get a new keyframe
gcErrors: 0, // count freed keyframe in JS, but kept in wsd.
missingContent: 0, // how many times rendered without content
invalidateCount: 0, // how many invalidations touched this tile
viewId: 0, // canonical view id
wireId: 0, // monotonic timestamp for optimizing fetch
invalidFrom: 0, // a wireId - for avoiding races on invalidation
lastRendered: new Date(),
hasContent: function() {
return this.imgDataCache || this.hasKeyframe();
},
needsFetch: function() {
return this.invalidFrom >= this.wireId || !this.hasContent();
},
hasKeyframe: function() {
return this.rawDeltas && this.rawDeltas.length > 0;
}
};
this._emptyTilesCount += 1;
this._tiles[key] = tile;
return tile;
},
_tileNeedsFetch: function(key) {
var tile = this._tiles[key];
return !tile || tile.needsFetch();
},
_getToolbarCommandsValues: function() {
for (var i = 0; i < this._map.unoToolbarCommands.length; i++) {
var command = this._map.unoToolbarCommands[i];
app.socket.sendMessage('commandvalues command=' + command);
}
},
_parseCellRange: function(cellRange) {
var strTwips = cellRange.match(/\d+/g);
var startCellAddress = [parseInt(strTwips[0]), parseInt(strTwips[1])];
var endCellAddress = [parseInt(strTwips[2]), parseInt(strTwips[3])];
return new L.Bounds(startCellAddress, endCellAddress);
},
_cellRangeToTwipRect: function(cellRange) {
var startCell = cellRange.getTopLeft();
var startCellRectPixel = this.sheetGeometry.getCellRect(startCell.x, startCell.y);
var topLeftTwips = this._corePixelsToTwips(startCellRectPixel.min);
var endCell = cellRange.getBottomRight();
var endCellRectPixel = this.sheetGeometry.getCellRect(endCell.x, endCell.y);
var bottomRightTwips = this._corePixelsToTwips(endCellRectPixel.max);
return new L.Bounds(topLeftTwips, bottomRightTwips);
},
_onMessage: function (textMsg, img) {
this._saveMessageForReplay(textMsg);
// 'tile:' is the most common message type; keep this the first.
if (textMsg.startsWith('tile:') || textMsg.startsWith('delta:')) {
this._onTileMsg(textMsg, img);
}
else if (textMsg.startsWith('commandvalues:')) {
this._onCommandValuesMsg(textMsg);
}
else if (textMsg.startsWith('cursorvisible:')) {
this._onCursorVisibleMsg(textMsg);
}
else if (textMsg.startsWith('downloadas:')) {
this._onDownloadAsMsg(textMsg);
}
else if (textMsg.startsWith('error:')) {
this._onErrorMsg(textMsg);
}
else if (textMsg.startsWith('getchildid:')) {
this._onGetChildIdMsg(textMsg);
}
else if (textMsg.startsWith('shapeselectioncontent:')) {
this._onShapeSelectionContent(textMsg);
}
else if (textMsg.startsWith('graphicselection:')) {
this._onGraphicSelectionMsg(textMsg);
}
else if (textMsg.startsWith('graphicinnertextarea:')) {
this._onGraphicInnerTextAreaMsg(textMsg);
}
else if (textMsg.startsWith('cellcursor:')) {
this._onCellCursorMsg(textMsg);
}
else if (textMsg.startsWith('celladdress:')) {
this._onCellAddressMsg(textMsg);
}
else if (textMsg.startsWith('cellformula:')) {
this._onCellFormulaMsg(textMsg);
}
else if (textMsg.startsWith('referencemarks:')) {
this._onReferencesMsg(textMsg);
}
else if (textMsg.startsWith('referenceclear:')) {
this._clearReferences();
}
else if (textMsg.startsWith('invalidatecursor:')) {
this._onInvalidateCursorMsg(textMsg);
}
else if (textMsg.startsWith('invalidatetiles:')) {
var payload = textMsg.substring('invalidatetiles:'.length + 1);
if (!payload.startsWith('EMPTY')) {
this._onInvalidateTilesMsg(textMsg);
}
else {
var msg = 'invalidatetiles: ';
// see invalidatetiles: in wsd/protocol.txt for structure
var tmp = payload.substring('EMPTY'.length).replaceAll(',', ' , ');
var tokens = tmp.split(/[ \n]+/);
var wireIdToken = undefined;
var commaargs = [];
var commaarg = false;
for (var i = 0; i < tokens.length; i++) {
if (tokens[i] === ',') {
commaarg = true;
continue;
}
if (commaarg) {
commaargs.push(tokens[i]);
commaarg = false;
}
else if (tokens[i].startsWith('wid=')) {
wireIdToken = tokens[i];
}
else if (tokens[i])
console.error('unsupported invalidatetile token: ' + tokens[i]);
}
if (this.isWriter()) {
msg += 'part=0 ';
} else {
var part = parseInt(commaargs.length > 0 ? commaargs[0] : '');
var mode = parseInt(commaargs.length > 1 ? commaargs[1] : '');
mode = (isNaN(mode) ? this._selectedMode : mode);
msg += 'part=' + (isNaN(part) ? this._selectedPart : part)
+ ((mode && mode !== 0) ? (' mode=' + mode) : '')
+ ' ';
}
msg += 'x=0 y=0 ';
msg += 'width=' + this._docWidthTwips + ' ';
msg += 'height=' + this._docHeightTwips;
if (wireIdToken !== undefined)
msg += ' ' + wireIdToken;
this._onInvalidateTilesMsg(msg);
}
}
else if (textMsg.startsWith('mousepointer:')) {
this._onMousePointerMsg(textMsg);
}
else if (textMsg.startsWith('renderfont:')) {
this._onRenderFontMsg(textMsg, img);
}
else if (textMsg.startsWith('searchnotfound:')) {
this._onSearchNotFoundMsg(textMsg);
}
else if (textMsg.startsWith('searchresultselection:')) {
this._onSearchResultSelection(textMsg);
}
else if (textMsg.startsWith('setpart:')) {
this._onSetPartMsg(textMsg);
}
else if (textMsg.startsWith('statechanged:')) {
this._onStateChangedMsg(textMsg);
}
else if (textMsg.startsWith('status:') || textMsg.startsWith('statusupdate:')) {
this._onStatusMsg(textMsg);
// update tiles and selection because mode could be changed
this._update();
this.updateAllGraphicViewSelections();
this.updateAllViewCursors();
this.updateAllTextViewSelection();
}
else if (textMsg.startsWith('textselection:')) {
this._onTextSelectionMsg(textMsg);
}
else if (textMsg.startsWith('textselectioncontent:')) {
let textMsgContent = textMsg.substr(22);
let textMsgHtml = '';
let textMsgPlainText = '';
if (textMsgContent.startsWith('{')) {
// Multiple formats: JSON.
let textMsgJson = JSON.parse(textMsgContent);
textMsgHtml = textMsgJson['text/html'];
textMsgPlainText = textMsgJson['text/plain;charset=utf-8'];
} else {
// Single format: as-is.
textMsgHtml = textMsgContent;
}
const hyperlinkTextBox = document.getElementById('hyperlink-text-box');
if (hyperlinkTextBox) {
// Hyperlink dialog is open, the text selection is for the link text
// widget.
const extracted = this._map.extractContent(textMsgHtml);
hyperlinkTextBox.value = extracted.trim();
} else if (this._map._clip) {
this._map._clip.setTextSelectionHTML(textMsgHtml, textMsgPlainText);
} else
// hack for ios and android to get selected text into hyperlink insertion dialog
this._selectedTextContent = textMsgHtml;
}
else if (textMsg.startsWith('clipboardchanged')) {
var jMessage = textMsg.substr(17);
jMessage = JSON.parse(jMessage);
if (jMessage.mimeType === 'text/plain') {
this._map._clip.setTextSelectionHTML(jMessage.content);
this._map._clip._execCopyCutPaste('copy');
}
}
else if (textMsg.startsWith('textselectionend:')) {
this._onTextSelectionEndMsg(textMsg);
}
else if (textMsg.startsWith('textselectionstart:')) {
this._onTextSelectionStartMsg(textMsg);
}
else if (textMsg.startsWith('cellselectionarea:')) {
this._onCellSelectionAreaMsg(textMsg);
}
else if (textMsg.startsWith('cellautofillarea:')) {
this._onCellAutoFillAreaMsg(textMsg);
}
else if (textMsg.startsWith('complexselection:')) {
if (this._map._clip)
this._map._clip.onComplexSelection(textMsg.substr('complexselection:'.length));
}
else if (textMsg.startsWith('windowpaint:')) {
this._onDialogPaintMsg(textMsg, img);
}
else if (textMsg.startsWith('window:')) {
this._onDialogMsg(textMsg);
}
else if (textMsg.startsWith('unocommandresult:')) {
this._onUnoCommandResultMsg(textMsg);
}
else if (textMsg.startsWith('rulerupdate:')) {
this._onRulerUpdate(textMsg);
}
else if (textMsg.startsWith('contextmenu:')) {
this._onContextMenuMsg(textMsg);
}
else if (textMsg.startsWith('invalidateviewcursor:')) {
this._onInvalidateViewCursorMsg(textMsg);
}
else if (textMsg.startsWith('viewcursorvisible:')) {
this._onViewCursorVisibleMsg(textMsg);
}
else if (textMsg.startsWith('cellviewcursor:')) {
this._onCellViewCursorMsg(textMsg);
}
else if (textMsg.startsWith('viewinfo:')) {
this._onViewInfoMsg(textMsg);
}
else if (textMsg.startsWith('textviewselection:')) {
this._onTextViewSelectionMsg(textMsg);
}
else if (textMsg.startsWith('graphicviewselection:')) {
this._onGraphicViewSelectionMsg(textMsg);
}
else if (textMsg.startsWith('tableselected:')) {
this._onTableSelectedMsg(textMsg);
}
else if (textMsg.startsWith('editor:')) {
this._updateEditor(textMsg);
}
else if (textMsg.startsWith('validitylistbutton:')) {
this._onValidityListButtonMsg(textMsg);
}
else if (textMsg.startsWith('validityinputhelp:')) {
this._onValidityInputHelpMsg(textMsg);
}
else if (textMsg.startsWith('signaturestatus:')) {
var signstatus = textMsg.substring('signaturestatus:'.length + 1);
this._map.onChangeSignStatus(signstatus);
}
else if (textMsg.startsWith('removesession')) {
var viewId = parseInt(textMsg.substring('removesession'.length + 1));
if (this._map._docLayer._viewId === viewId) {
this._map.fire('postMessage', {msgId: 'close', args: {EverModified: this._map._everModified, Deprecated: true}});
this._map.fire('postMessage', {msgId: 'UI_Close', args: {EverModified: this._map._everModified}});
if (!this._map._disableDefaultAction['UI_Close']) {
this._map.remove();
}
}
}
else if (textMsg.startsWith('calcfunctionlist:')) {
this._onCalcFunctionListMsg(textMsg.substring('calcfunctionlist:'.length + 1));
}
else if (textMsg.startsWith('tooltip:')) {
var tooltipInfo = JSON.parse(textMsg.substring('tooltip:'.length + 1));
if (tooltipInfo.type === 'formulausage') {
this._onCalcFunctionUsageMsg(tooltipInfo.text);
}
else if (tooltipInfo.type === 'generaltooltip') {
var tooltipInfo = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
this._map.uiManager.showDocumentTooltip(tooltipInfo);
}
else {
console.error('unknown tooltip type');
}
}
else if (textMsg.startsWith('tabstoplistupdate:')) {
this._onTabStopListUpdate(textMsg);
}
else if (textMsg.startsWith('context:')) {
var message = textMsg.substring('context:'.length + 1);
message = message.split(' ');
if (message.length > 1) {
var old = this._map.context || {};
this._map.context = {appId: message[0], context: message[1]};
this._map.fire('contextchange', {appId: message[0], context: message[1], oldAppId: old.appId, oldContext: old.context});
}
}
else if (textMsg.startsWith('formfieldbutton:')) {
this._onFormFieldButtonMsg(textMsg);
}
else if (textMsg.startsWith('canonicalidchange:')) {
var payload = textMsg.substring('canonicalidchange:'.length + 1);
var viewRenderedState = payload.split('=')[3].split(' ')[0];
if (this._debug.overlayOn) {
var viewId = payload.split('=')[1].split(' ')[0];
var canonicalId = payload.split('=')[2].split(' ')[0];
this._debug.setOverlayMessage('canonicalViewId',
'Canonical id changed to: ' + canonicalId + ' for view id: ' + viewId + ' with view renderend state: ' + viewRenderedState
);
}
if (!this._canonicalIdInitialized) {
this._canonicalIdInitialized = true;
this._update();
} else {
this._requestNewTiles();
this._invalidateAllPreviews();
this.redraw();
}
}
else if (textMsg.startsWith('comment:')) {
var obj = JSON.parse(textMsg.substring('comment:'.length + 1));
app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).onACKComment(obj);
}
else if (textMsg.startsWith('redlinetablemodified:')) {
obj = JSON.parse(textMsg.substring('redlinetablemodified:'.length + 1));
app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).onACKComment(obj);
}
else if (textMsg.startsWith('redlinetablechanged:')) {
obj = JSON.parse(textMsg.substring('redlinetablechanged:'.length + 1));
app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).onACKComment(obj);
}
else if (textMsg.startsWith('applicationbackgroundcolor:')) {
app.sectionContainer.setClearColor('#' + textMsg.substring('applicationbackgroundcolor:'.length + 1).trim());
app.sectionContainer.requestReDraw();
}
else if (textMsg.startsWith('documentbackgroundcolor:')) {
app.sectionContainer.setDocumentBackgroundColor('#' + textMsg.substring('documentbackgroundcolor:'.length + 1).trim());
}
else if (textMsg.startsWith('contentcontrol:')) {
textMsg = textMsg.substring('contentcontrol:'.length + 1);
if (!app.sectionContainer.doesSectionExist(L.CSections.ContentControl.name)) {
app.sectionContainer.addSection(new app.definitions.ContentControlSection());
}
var section = app.sectionContainer.getSectionWithName(L.CSections.ContentControl.name);
section.drawContentControl(JSON.parse(textMsg));
}
else if (textMsg.startsWith('versionbar:')) {
obj = JSON.parse(textMsg.substring('versionbar:'.length + 1));
this._map.fire('versionbar', obj);
}
else if (textMsg.startsWith('a11yfocuschanged:')) {
obj = JSON.parse(textMsg.substring('a11yfocuschanged:'.length + 1));
var listPrefixLength = obj.listPrefixLength !== undefined ? parseInt(obj.listPrefixLength) : 0;
this._map._textInput.onAccessibilityFocusChanged(
obj.content, parseInt(obj.position), parseInt(obj.start), parseInt(obj.end),
listPrefixLength, parseInt(obj.force) > 0);
}
else if (textMsg.startsWith('a11ycaretchanged:')) {
obj = JSON.parse(textMsg.substring('a11yfocuschanged:'.length + 1));
this._map._textInput.onAccessibilityCaretChanged(parseInt(obj.position));
}
else if (textMsg.startsWith('a11ytextselectionchanged:')) {
obj = JSON.parse(textMsg.substring('a11ytextselectionchanged:'.length + 1));
this._map._textInput.onAccessibilityTextSelectionChanged(parseInt(obj.start), parseInt(obj.end));
}
else if (textMsg.startsWith('a11yfocusedcellchanged:')) {
obj = JSON.parse(textMsg.substring('a11yfocusedcellchanged:'.length + 1));
var outCount = obj.outCount !== undefined ? parseInt(obj.outCount) : 0;
var inList = obj.inList !== undefined ? obj.inList : [];
var row = parseInt(obj.row);
var col = parseInt(obj.col);
var rowSpan = obj.rowSpan !== undefined ? parseInt(obj.rowSpan) : 1;
var colSpan = obj.colSpan !== undefined ? parseInt(obj.colSpan) : 1;
this._map._textInput.onAccessibilityFocusedCellChanged(
outCount, inList, row, col, rowSpan, colSpan, obj.paragraph);
}
else if (textMsg.startsWith('a11yeditinginselectionstate:')) {
obj = JSON.parse(textMsg.substring('a11yeditinginselectionstate:'.length + 1));
this._map._textInput.onAccessibilityEditingInSelectionState(
parseInt(obj.cell) > 0, parseInt(obj.enabled) > 0, obj.selection, obj.paragraph);
}
else if (textMsg.startsWith('a11yselectionchanged:')) {
obj = JSON.parse(textMsg.substring('a11yselectionchanged:'.length + 1));
this._map._textInput.onAccessibilitySelectionChanged(
parseInt(obj.cell) > 0, obj.action, obj.name, obj.text);
}
else if (textMsg.startsWith('a11yfocusedparagraph:')) {
obj = JSON.parse(textMsg.substring('a11yfocusedparagraph:'.length + 1));
this._map._textInput.setA11yFocusedParagraph(
obj.content, parseInt(obj.position), parseInt(obj.start), parseInt(obj.end));
}
else if (textMsg.startsWith('a11ycaretposition:')) {
var pos = textMsg.substring('a11ycaretposition:'.length + 1);
this._map._textInput.setA11yCaretPosition(parseInt(pos));
}
else if (textMsg.startsWith('colorpalettes:')) {
var json = JSON.parse(textMsg.substring('colorpalettes:'.length + 1));
for (var key in json) {
if (app.colorPalettes[key]) {
app.colorPalettes[key].colors = json[key];
} else {
window.app.console.warn('Unknown palette: "' + key + '"');
}
}
// Remove empty palettes, eg. Document colors in Impress are empty
for (var key in app.colorPalettes) {
if (!app.colorPalettes[key].colors || !app.colorPalettes[key].colors.length) {
delete app.colorPalettes[key];
}
}
}
},
_onTabStopListUpdate: function (textMsg) {
textMsg = textMsg.substring('tabstoplistupdate:'.length + 1);
var json = JSON.parse(textMsg);
this._map.fire('tabstoplistupdate', json);
},
_onCommandValuesMsg: function (textMsg) {
var jsonIdx = textMsg.indexOf('{');
if (jsonIdx === -1) {
return;
}
var obj = JSON.parse(textMsg.substring(jsonIdx));
if (obj.commandName === '.uno:DocumentRepair') {
this._onDocumentRepair(obj);
}
else if (obj.commandName === '.uno:CellCursor') {
this._onCellCursorMsg(obj.commandValues);
}
else if (this._map.unoToolbarCommands.indexOf(obj.commandName) !== -1) {
this._toolbarCommandValues[obj.commandName] = obj.commandValues;
this._map.fire('updatetoolbarcommandvalues', {
commandName: obj.commandName,
commandValues: obj.commandValues
});
}
else {
this._map.fire('commandvalues', {
commandName: obj.commandName,
commandValues: obj.commandValues
});
}
},
_onCellAddressMsg: function (textMsg) {
// When the user moves the focus to a different cell, a 'cellformula'
// message is received from coolwsd, *then* a 'celladdress' message.
var address = textMsg.substring(13);
if (this._map._clip && !this._map['wopi'].DisableCopy) {
this._map._clip.setTextSelectionText(this._lastFormula);
}
this._map.fire('celladdress', {address: address});
},
_onCellFormulaMsg: function (textMsg) {
// When a 'cellformula' message from coolwsd is received,
// store the text contents of the cell, but don't push
// them to the clipboard container (yet).
// This is done because coolwsd will send several 'cellformula'
// messages during text composition, and resetting the contents
// of the clipboard container mid-composition will easily break it.
var formula = textMsg.substring(13);
this._lastFormula = formula;
this._map.fire('cellformula', {formula: formula});
},
_onCalcFunctionUsageMsg: function (textMsg) {
var pos = this._map._docLayer._twipsToLatLng({ x: this._lastVisibleCursorRef.x2, y: this._lastVisibleCursorRef.y1 });
this._map.uiManager.showFormulaTooltip(textMsg, pos);
},
_onCalcFunctionListMsg: function (textMsg) {
if (textMsg.startsWith('hidetip')) {
this._map.uiManager.hideFormulaTooltip();
}
else {
var funcData = JSON.parse(textMsg);
if (window.mode.isMobile()) {
this._closeMobileWizard();
var data = {
id: 'funclist',
type: '',
text: _('Functions'),
enabled: true,
children: []
};
if (funcData.categories)
this._onCalcFunctionListWithCategories(funcData, data);
else
this._onCalcFunctionList(funcData, data);
if (funcData.wholeList)
this._map._functionWizardData = data;
this._openMobileWizard(data);
}
else {
var pos = this._map._docLayer._twipsToLatLng({ x: this._lastVisibleCursorRef.x2, y: this._lastVisibleCursorRef.y1 });
var tooltipinfo = this._getFunctionList(textMsg);
this._map.uiManager.showFormulaTooltip(tooltipinfo, pos);
}
}
},
_getCalcFunctionListEntry: function(name, category, index, signature, description) {
return {
id: '',
type: 'calcfuncpanel',
text: name,
functionName: name,
index: index,
category: category,
enabled: true,
children: [
{
id: '',
type: 'fixedtext',
html: '<div class="func-info-sig">' + signature + '</div>' + '<div class="func-info-desc">' + description + '</div>',
enabled: true,
style: 'func-info'
}
]
};
},
_onCalcFunctionList: function (funcList, data) {
var entries = data.children;
for (var idx = 0; idx < funcList.length; ++idx) {
var func = funcList[idx];
var name = func.signature.split('(')[0];
entries.push(this._getCalcFunctionListEntry(
name, undefined, func.index, func.signature, func.description));
}
},
_onCalcFunctionListWithCategories: function (funcData, data) {
var categoryList = funcData.categories;
var categoryEntries = data.children;
for (var idx = 0; idx < categoryList.length; ++idx) {
var category = categoryList[idx];
var categoryEntry = {
id: '',
type: 'panel',
text: category.name,
index: idx,
enabled: true,
children: []
};
categoryEntries.push(categoryEntry);
}
var funcList = funcData.functions;
for (idx = 0; idx < funcList.length; ++idx) {
var func = funcList[idx];
var name = func.signature.split('(')[0];
var funcEntries = categoryEntries[func.category].children;
funcEntries.push(this._getCalcFunctionListEntry(
name, func.category, func.index, func.signature, func.description));
}
},
_onCursorVisibleMsg: function(textMsg) {
var command = textMsg.match('cursorvisible: true');
app.file.textCursor.visible = command ? true : false;
this._removeSelection();
this._onUpdateCursor();
},
_setCursorVisible: function() {
app.file.textCursor.visible = true;
},
_onDownloadAsMsg: function (textMsg) {
var command = app.socket.parseServerCmd(textMsg);
var parser = document.createElement('a');
parser.href = window.host;
var url = window.makeHttpUrlWopiSrc('/' + this._map.options.urlPrefix + '/',
this._map.options.doc, '/download/' + command.downloadid);
this._map.hideBusy();
if (this._map['wopi'].DownloadAsPostMessage) {
this._map.fire('postMessage', {msgId: 'Download_As', args: {Type: command.id, URL: url}});
}
else if (command.id === 'print') {
if (this._map.options.print === false || L.Browser.cypressTest) {
// open the pdf in a new tab, it can be printed directly in the browser's pdf viewer
url = window.makeHttpUrlWopiSrc('/' + this._map.options.urlPrefix + '/',
this._map.options.doc, '/download/' + command.downloadid,
'attachment=0');
if ('processCoolUrl' in window) {
url = window.processCoolUrl({ url: url, type: 'print' });
}
window.open(url, '_blank');
}
else {
if ('processCoolUrl' in window) {
url = window.processCoolUrl({ url: url, type: 'print' });
}
this._map.fire('filedownloadready', {url: url});
}
}
else if (command.id === 'slideshow') {
this._map.fire('slidedownloadready', {url: url});
}
else if (command.id === 'export') {
if ('processCoolUrl' in window) {
url = window.processCoolUrl({ url: url, type: 'export' });
}
// Don't do a real download during testing
if (!L.Browser.cypressTest)
this._map._fileDownloader.src = url;
else
this._map._fileDownloader.setAttribute('data-src', url);
}
},
_onErrorMsg: function (textMsg) {
var command = app.socket.parseServerCmd(textMsg);
// let's provide some convenience error codes for the UI
var errorId = 1; // internal error
if (command.errorCmd === 'load') {
errorId = 2; // document cannot be loaded
}
else if (command.errorCmd === 'save' || command.errorCmd === 'saveas') {
errorId = 5; // document cannot be saved
}
var errorCode = -1;
if (command.errorCode !== undefined) {
errorCode = command.errorCode;
}
this._map.fire('error', {cmd: command.errorCmd, kind: command.errorKind, id: errorId, code: errorCode});
},
_onGetChildIdMsg: function (textMsg) {
var command = app.socket.parseServerCmd(textMsg);
this._map.fire('childid', {id: command.id});
},
_isGraphicAngleDivisibleBy90: function() {
return (this._graphicSelectionAngle % 9000 === 0);
},
_shouldScaleUniform: function(extraInfo) {
return (!this._isGraphicAngleDivisibleBy90() || extraInfo.isWriterGraphic || extraInfo.type === 22);
},
_onShapeSelectionContent: function (textMsg) {
textMsg = textMsg.substring('shapeselectioncontent:'.length + 1);
if (this._graphicMarker) {
var extraInfo = this._graphicSelection.extraInfo;
if (extraInfo.id) {
this._map._cacheSVG[extraInfo.id] = textMsg;
}
var wasVisibleSVG = this._graphicMarker._hasVisibleEmbeddedSVG();
this._graphicMarker.removeEmbeddedSVG();
// video is handled in _onEmbeddedVideoContent
var isVideoSVG = textMsg.indexOf('<video') !== -1;
if (isVideoSVG) {
this._map._cacheSVG[extraInfo.id] = undefined;
} else {
this._graphicMarker.addEmbeddedSVG(textMsg);
if (wasVisibleSVG)
this._graphicMarker._showEmbeddedSVG();
}
}
},
// shows the video inside current selection marker
_onEmbeddedVideoContent: function (textMsg) {
if (!this._graphicMarker)
return;
// Remove other view selection as it interferes with playing the media.
for (var viewId in this._graphicViewMarkers) {
if (viewId !== this._viewId && this._map._viewInfo[viewId]) {
var viewMarker = this._graphicViewMarkers[viewId].marker;
if (viewMarker)
this._viewLayerGroup.removeLayer(viewMarker);
}
}
var videoDesc = JSON.parse(textMsg);
if (this._graphicSelectionTwips) {
var topLeftPoint = this._twipsToCssPixels(
this._graphicSelectionTwips.getTopLeft(), this._map.getZoom());
var bottomRightPoint = this._twipsToCssPixels(
this._graphicSelectionTwips.getBottomRight(), this._map.getZoom());
videoDesc.width = bottomRightPoint.x - topLeftPoint.x;
videoDesc.height = bottomRightPoint.y - topLeftPoint.y;
}
// proxy cannot identify RouteToken if it is encoded
var routeTokenIndex = videoDesc.url.indexOf('%26RouteToken=');
if (routeTokenIndex != -1) {
videoDesc.url = videoDesc.url.replace('%26RouteToken=', '&amp;RouteToken=');
}
var videoToInsert = '<?xml version="1.0" encoding="UTF-8"?>\
<foreignObject xmlns="http://www.w3.org/2000/svg" overflow="visible" width="'
+ videoDesc.width + '" height="' + videoDesc.height + '">\
<body xmlns="http://www.w3.org/1999/xhtml">\
<video controls="controls" width="' + videoDesc.width + '" height="'
+ videoDesc.height + '">\
<source src="' + videoDesc.url + '" type="' + videoDesc.mimeType + '"/>\
</video>\
</body>\
</foreignObject>';
this._graphicMarker.addEmbeddedVideo(videoToInsert);
},
_resetSelectionRanges: function() {
this._graphicSelectionTwips = new L.Bounds(new L.Point(0, 0), new L.Point(0, 0));
this._graphicSelection = new L.LatLngBounds(new L.LatLng(0, 0), new L.LatLng(0, 0));
this._hasActiveSelection = false;
},
_openMobileWizard: function(data) {
this._map.fire('mobilewizard', {data: data});
},
_closeMobileWizard: function() {
this._map.fire('closemobilewizard');
},
_extractAndSetGraphicSelection: function(messageJSON) {
var calcRTL = this.isCalcRTL();
var signX = calcRTL ? -1 : 1;
var hasExtraInfo = messageJSON.length > 5;
var hasGridOffset = false;
var extraInfo = null;
if (hasExtraInfo) {
extraInfo = messageJSON[5];
if (extraInfo.gridOffsetX || extraInfo.gridOffsetY) {
this._shapeGridOffset = new L.Point(signX * parseInt(extraInfo.gridOffsetX), parseInt(extraInfo.gridOffsetY));
hasGridOffset = true;
}
}
// Calc RTL: Negate positive X coordinates from core if grid offset is available.
signX = hasGridOffset && calcRTL ? -1 : 1;
var topLeftTwips = new L.Point(signX * messageJSON[0], messageJSON[1]);
var offset = new L.Point(signX * messageJSON[2], messageJSON[3]);
var bottomRightTwips = topLeftTwips.add(offset);
if (hasGridOffset) {
this._graphicSelectionTwips = new L.Bounds(topLeftTwips.add(this._shapeGridOffset), bottomRightTwips.add(this._shapeGridOffset));
} else {
this._graphicSelectionTwips = this._getGraphicSelectionRectangle(
new L.Bounds(topLeftTwips, bottomRightTwips));
}
this._graphicSelection = new L.LatLngBounds(
this._twipsToLatLng(this._graphicSelectionTwips.getTopLeft(), this._map.getZoom()),
this._twipsToLatLng(this._graphicSelectionTwips.getBottomRight(), this._map.getZoom()));
this._graphicSelection.extraInfo = extraInfo;
},
renderDarkOverlay: function () {
var zoom = this._map.getZoom();
var northEastPoint = this._latLngToCorePixels(this._graphicSelection.getNorthEast(), zoom);
var southWestPoint = this._latLngToCorePixels(this._graphicSelection.getSouthWest(), zoom);
if (this.isCalcRTL()) {
// Dark overlays (like any other overlay) need regular document coordinates.
// But in calc-rtl mode, charts (like shapes) have negative x document coordinate
// internal representation.
northEastPoint.x = Math.abs(northEastPoint.x);
southWestPoint.x = Math.abs(southWestPoint.x);
}
var bounds = new L.Bounds(northEastPoint, southWestPoint);
this._oleCSelections.setPointSet(CPointSet.fromBounds(bounds));
},
_onGraphicInnerTextAreaMsg: function (textMsg) {
var msgData = JSON.parse(textMsg.substr('graphicinnertextarea: "innerTextRect":'.length));
this._onUpdateGraphicInnerTextArea(msgData, true /*force add layer*/);
},
_onGraphicSelectionMsg: function (textMsg) {
if (this._map.hyperlinkPopup !== null) {
this._closeURLPopUp();
}
if (textMsg.match('EMPTY')) {
this._resetSelectionRanges();
}
else if (textMsg.match('INPLACE EXIT')) {
this._oleCSelections.clear();
}
else if (textMsg.match('INPLACE')) {
if (this._oleCSelections.empty()) {
textMsg = '[' + textMsg.substr('graphicselection:'.length) + ']';
try {
var msgData = JSON.parse(textMsg);
if (msgData.length > 1)
this._extractAndSetGraphicSelection(msgData);
}
catch (error) {
window.app.console.warn('cannot parse graphicselection command');
}
this.renderDarkOverlay();
this._graphicSelection = new L.LatLngBounds(new L.LatLng(0, 0), new L.LatLng(0, 0));
this._onUpdateGraphicSelection();
}
}
else {
textMsg = '[' + textMsg.substr('graphicselection:'.length) + ']';
msgData = JSON.parse(textMsg);
this._extractAndSetGraphicSelection(msgData);
// Update the dark overlay on zooming & scrolling
if (!this._oleCSelections.empty()) {
this._oleCSelections.clear();
this.renderDarkOverlay();
}
this._graphicSelectionAngle = (msgData.length > 4) ? msgData[4] : 0;
if (this._graphicSelection.extraInfo) {
var dragInfo = this._graphicSelection.extraInfo.dragInfo;
if (dragInfo && dragInfo.dragMethod === 'PieSegmentDragging') {
dragInfo.initialOffset /= 100.0;
var dragDir = dragInfo.dragDirection;
dragInfo.dragDirection = this._twipsToPixels(new L.Point(dragDir[0], dragDir[1]));
dragDir = dragInfo.dragDirection;
dragInfo.range2 = dragDir.x * dragDir.x + dragDir.y * dragDir.y;
}
}
// defaults
var extraInfo = this._graphicSelection.extraInfo;
if (extraInfo) {
if (extraInfo.isDraggable === undefined)
extraInfo.isDraggable = true;
if (extraInfo.isResizable === undefined)
extraInfo.isResizable = true;
if (extraInfo.isRotatable === undefined)
extraInfo.isRotatable = true;
}
// Workaround for tdf#123874. For some reason the handling of the
// shapeselectioncontent messages that we get back causes the WebKit process
// to crash on iOS.
// Note2: scroll to frame in writer would result an error:
// svgexport.cxx:810: ...UnknownPropertyException message: "Background
var isFrame = extraInfo.type == 601 && !extraInfo.isWriterGraphic;
if (!window.ThisIsTheiOSApp && this._graphicSelection.extraInfo.isDraggable && !this._graphicSelection.extraInfo.svg
&& !isFrame)
{
app.socket.sendMessage('rendershapeselection mimetype=image/svg+xml');
}
// scroll to selected graphics, if it has no cursor
if (!this.isWriter() && !this._isEmptyRectangle(this._graphicSelection)
&& this._allowViewJump()) {
var docLayer = this._map._docLayer;
var paneRectsInLatLng = this.getPaneLatLngRectangles();
if (!this._graphicSelection.isInAny(paneRectsInLatLng) &&
!(this._selectionHandles.start && this._selectionHandles.start.isDragged) &&
!(this._selectionHandles.end && this._selectionHandles.end.isDragged) &&
!(docLayer._followEditor || docLayer._followUser) &&
!this._map.calcInputBarHasFocus()) {
this.scrollToPos(this._graphicSelection.getNorthWest());
}
}
}
// Graphics are by default complex selections, unless Core tells us otherwise.
if (this._map._clip)
this._map._clip.onComplexSelection('');
// Reset text selection - important for textboxes in Impress
if (this._selectionContentRequest)
clearTimeout(this._selectionContentRequest);
this._onMessage('textselectioncontent:');
this._onUpdateGraphicSelection();
if (msgData && msgData.length > 5) {
var extraInfo = msgData[5];
if (extraInfo.url !== undefined) {
this._onEmbeddedVideoContent(JSON.stringify(extraInfo));
}
}
},
_onGraphicViewSelectionMsg: function (textMsg) {
var obj = JSON.parse(textMsg.substring('graphicviewselection:'.length + 1));
var viewId = parseInt(obj.viewId);
// Ignore if viewid is ours or not in our db
if (viewId === this._viewId || !this._map._viewInfo[viewId]) {
return;
}
var strTwips = obj.selection.match(/\d+/g);
this._graphicViewMarkers[viewId] = this._graphicViewMarkers[viewId] || {};
this._graphicViewMarkers[viewId].part = parseInt(obj.part);
this._graphicViewMarkers[viewId].mode = (obj.mode !== undefined) ? parseInt(obj.mode) : 0;
if (strTwips != null) {
var topLeftTwips = new L.Point(parseInt(strTwips[0]), parseInt(strTwips[1]));
var offset = new L.Point(parseInt(strTwips[2]), parseInt(strTwips[3]));
var bottomRightTwips = topLeftTwips.add(offset);
var boundRectTwips = this._getGraphicSelectionRectangle(
new L.Bounds(topLeftTwips, bottomRightTwips));
this._graphicViewMarkers[viewId].bounds = new L.LatLngBounds(
this._twipsToLatLng(boundRectTwips.getTopLeft(), this._map.getZoom()),
this._twipsToLatLng(boundRectTwips.getBottomRight(), this._map.getZoom()));
}
else {
this._graphicViewMarkers[viewId].bounds = L.LatLngBounds.createDefault();
}
this._onUpdateGraphicViewSelection(viewId);
if (this.isCalc()) {
this._saveMessageForReplay(textMsg, viewId);
}
},
_onCellCursorMsg: function (textMsg) {
var autofillMarkerSection = app.sectionContainer.getSectionWithName(L.CSections.AutoFillMarker.name);
var oldCursorAddress = app.calc.cellAddress.clone();
if (textMsg.match('EMPTY')) {
app.calc.cellCursorVisible = false;
if (autofillMarkerSection)
autofillMarkerSection.calculatePositionViaCellCursor(null);
if (this._map._clip)
this._map._clip.clearSelection();
}
else {
var strTwips = textMsg.match(/\d+/g);
var topLeftTwips = new L.Point(parseInt(strTwips[0]), parseInt(strTwips[1]));
var offset = new L.Point(parseInt(strTwips[2]), parseInt(strTwips[3]));
var bottomRightTwips = topLeftTwips.add(offset);
let _cellCursorTwips = this._convertToTileTwipsSheetArea(new L.Bounds(topLeftTwips, bottomRightTwips));
app.calc.cellAddress = new app.definitions.simplePoint(parseInt(strTwips[4]), parseInt(strTwips[5]));
let tempRectangle = _cellCursorTwips.toRectangle();
app.calc.cellCursorRectangle = new app.definitions.simpleRectangle(tempRectangle[0], tempRectangle[1], tempRectangle[2], tempRectangle[3]);
app.calc.cellCursorVisible = true;
app.sectionContainer.onCellAddressChanged();
if (autofillMarkerSection)
autofillMarkerSection.calculatePositionViaCellCursor([app.calc.cellCursorRectangle.pX2, app.calc.cellCursorRectangle.pY2]);
}
var onPgUpDn = false;
if (app.calc.cellCursorVisible && this._prevCellCursor && !this._prevCellCursor.equals(app.calc.cellCursorRectangle.toArray())) {
if ((this._cellCursorOnPgUp && this._cellCursorOnPgUp.equals(this._prevCellCursor.toArray())) ||
(this._cellCursorOnPgDn && this._cellCursorOnPgDn.equals(this._prevCellCursor.toArray()))) {
onPgUpDn = true;
}
this._prevCellCursor = app.calc.cellCursorRectangle.clone();
}
var sameAddress = oldCursorAddress.equals(app.calc.cellAddress.toArray());
var scrollToCursor = this._sheetSwitch.tryRestore(sameAddress, this._selectedPart);
this._onUpdateCellCursor(onPgUpDn, scrollToCursor, sameAddress);
// Remove input help if there is any:
this._removeInputHelpMarker();
},
_removeInputHelpMarker: function() {
if (this._inputHelpPopUp) {
this._map.removeLayer(this._inputHelpPopUp);
this._inputHelpPopUp = null;
}
},
_onDocumentRepair: function (textMsg) {
if (!this._docRepair) {
this._docRepair = L.control.documentRepair();
}
if (!this._docRepair.isVisible()) {
this._docRepair.addTo(this._map);
this._docRepair.fillActions(textMsg);
this._docRepair.show();
}
},
_onMousePointerMsg: function (textMsg) {
textMsg = textMsg.substring(14); // "mousepointer: "
textMsg = Cursor.getCustomCursor(textMsg) || textMsg;
var mapPane = $('.leaflet-pane.leaflet-map-pane');
if (mapPane.css('cursor') !== textMsg) {
mapPane.css('cursor', textMsg);
}
},
_setupClickFuncForId: function(targetId, func) {
var target = document.getElementById(targetId);
target.style.cursor = 'pointer';
target.onclick = target.ontouchend = func;
},
_getFunctionList: function(textMsg) {
var maxSuggestion = 3;
var functionNameList = [];
var resultText = '';
var currentFuncDescription = '';
var suggestionArray = JSON.parse(textMsg);
if (suggestionArray.length < maxSuggestion) { maxSuggestion = suggestionArray.length; }
for (var i = 0; i < maxSuggestion; i++) {
if (i == 0)
currentFuncDescription = suggestionArray[i].description;
var signature = suggestionArray[i].signature;
functionNameList.push(signature.substring(0,signature.indexOf('(')));
}
for (var i = 0; i < maxSuggestion; i++) {
if (i == 0)
resultText = resultText + '[' + functionNameList[i] + ']';
else
resultText = resultText + ', ' + functionNameList[i];
}
var remainingFuncCount = suggestionArray.length - maxSuggestion;
if (remainingFuncCount > 0)
resultText = resultText + ' ' + _('and %COUNT more').replace('%COUNT', remainingFuncCount);
resultText = resultText + ' : ' + currentFuncDescription;
return resultText;
},
_showURLPopUp: function(position, url) {
var parent = L.DomUtil.create('div', '');
L.DomUtil.createWithId('div', 'hyperlink-pop-up-preview', parent);
var link = L.DomUtil.createWithId('a', 'hyperlink-pop-up', parent);
link.innerText = url;
var copyBtn = L.DomUtil.createWithId('div', 'hyperlink-pop-up-copy', parent);
L.DomUtil.addClass(copyBtn, 'hyperlink-popup-btn');
copyBtn.setAttribute('title', _('Copy link location'));
var imgCopyBtn = L.DomUtil.create('img', 'hyperlink-pop-up-copyimg', copyBtn);
L.LOUtil.setImage(imgCopyBtn, 'lc_copyhyperlinklocation.svg', this._map);
imgCopyBtn.setAttribute('width', 18);
imgCopyBtn.setAttribute('height', 18);
imgCopyBtn.setAttribute('style', 'padding: 4px');
var editBtn = L.DomUtil.createWithId('div', 'hyperlink-pop-up-edit', parent);
L.DomUtil.addClass(editBtn, 'hyperlink-popup-btn');
editBtn.setAttribute('title', _('Edit link'));
var imgEditBtn = L.DomUtil.create('img', 'hyperlink-pop-up-editimg', editBtn);
L.LOUtil.setImage(imgEditBtn, 'lc_edithyperlink.svg', this._map);
imgEditBtn.setAttribute('width', 18);
imgEditBtn.setAttribute('height', 18);
imgEditBtn.setAttribute('style', 'padding: 4px');
var removeBtn = L.DomUtil.createWithId('div', 'hyperlink-pop-up-remove', parent);
L.DomUtil.addClass(removeBtn, 'hyperlink-popup-btn');
removeBtn.setAttribute('title', _('Remove link'));
var imgRemoveBtn = L.DomUtil.create('img', 'hyperlink-pop-up-removeimg', removeBtn);
L.LOUtil.setImage(imgRemoveBtn, 'lc_removehyperlink.svg', this._map);
imgRemoveBtn.setAttribute('width', 18);
imgRemoveBtn.setAttribute('height', 18);
imgRemoveBtn.setAttribute('style', 'padding: 4px');
this._map.hyperlinkPopup = new L.Popup({className: 'hyperlink-popup', closeButton: false, closeOnClick: false, autoPan: false})
.setHTMLContent(parent)
.setLatLng(position)
.openOn(this._map);
document.getElementById('hyperlink-pop-up').title = url;
var offsetDiffTop = $('.hyperlink-popup').offset().top - $('#map').offset().top;
var offsetDiffLeft = $('.hyperlink-popup').offset().left - $('#map').offset().left;
if (offsetDiffTop < 10) this._movePopUpBelow();
if (offsetDiffLeft < 10) this._movePopUpRight();
var map_ = this._map;
this._setupClickFuncForId('hyperlink-pop-up', function() {
if (!url.startsWith('#'))
map_.fire('warn', {url: url, map: map_, cmd: 'openlink'});
else
map_.sendUnoCommand('.uno:JumpToMark?Bookmark:string=' + encodeURIComponent(url.substring(1)));
});
this._setupClickFuncForId('hyperlink-pop-up-copy', function () {
map_.sendUnoCommand('.uno:CopyHyperlinkLocation');
});
this._setupClickFuncForId('hyperlink-pop-up-edit', function () {
map_.sendUnoCommand('.uno:EditHyperlink');
});
this._setupClickFuncForId('hyperlink-pop-up-remove', function () {
map_.sendUnoCommand('.uno:RemoveHyperlink');
});
if (this._map['wopi'].EnableRemoteLinkPicker)
this._map.fire('postMessage', { msgId: 'Action_GetLinkPreview', args: { url: url } });
},
_movePopUpBelow: function() {
var popUp = $('.hyperlink-popup').first();
var bottom = parseInt(popUp.css('bottom')) - popUp.height();
popUp.css({
'bottom': bottom ? bottom + 'px': '',
'display': 'flex',
'flex-direction': 'column-reverse'
});
$('.leaflet-popup-tip-container').first().css('transform', 'rotate(180deg)');
},
_movePopUpRight: function() {
$('.leaflet-popup-content-wrapper').first().css({
'position': 'relative',
'left': (this._map.hyperlinkPopup._containerWidth / 2)
});
$('.leaflet-popup-tip-container').first().css({
'left': '25px'
});
},
_closeURLPopUp: function() {
this._map.closePopup(this._map.hyperlinkPopup);
this._map.hyperlinkPopup = null;
},
_onInvalidateCursorMsg: function (textMsg) {
textMsg = textMsg.substring('invalidatecursor:'.length + 1);
var obj = JSON.parse(textMsg);
var recCursor = this._getEditCursorRectangle(obj);
if (recCursor === undefined || this.persistCursorPositionInWriter) {
this.persistCursorPositionInWriter = false;
return;
}
app.file.textCursor.visible = true;
// tells who trigerred cursor invalidation, but recCursors is stil "our"
var modifierViewId = parseInt(obj.viewId);
var weAreModifier = (modifierViewId === this._viewId);
this._cursorAtMispelledWord = obj.mispelledWord ? Boolean(parseInt(obj.mispelledWord)).valueOf() : false;
// Remember the last position of the caret (in core pixels).
this._cursorPreviousPositionCorePixels = app.file.textCursor.rectangle.clone();
app.file.textCursor.rectangle = new app.definitions.simpleRectangle(recCursor.getTopLeft().x, recCursor.getTopLeft().y, recCursor.getSize().x, recCursor.getSize().y);
if (this._docType === 'text') {
app.sectionContainer.onCursorPositionChanged();
}
var docLayer = this._map._docLayer;
if ((docLayer._followEditor || docLayer._followUser) && this._map.lastActionByUser) {
this._map._setFollowing(false, null);
}
this._map.lastActionByUser = false;
this._map.hyperlinkUnderCursor = obj.hyperlink;
this._closeURLPopUp();
if (obj.hyperlink && obj.hyperlink.link) {
this._showURLPopUp(this._map._docLayer._twipsToLatLng({ x: app.file.textCursor.rectangle.x1, y: app.file.textCursor.rectangle.y1 }), obj.hyperlink.link);
}
if (!this._map.editorHasFocus() && app.file.textCursor.visible && weAreModifier) {
// Regain cursor if we had been out of focus and now have input.
// Unless the focus is in the Calc Formula-Bar, don't steal the focus.
if (!this._map.calcInputBarHasFocus())
this._map.fire('editorgotfocus');
}
//first time document open, set last cursor position
if (!this.lastCursorPos)
this.lastCursorPos = app.file.textCursor.rectangle.clone();
var updateCursor = false;
if (!this.lastCursorPos.equals(app.file.textCursor.rectangle.toArray())) {
updateCursor = true;
this.lastCursorPos = app.file.textCursor.rectangle.clone();
}
// If modifier view is different than the current view
// we'll keep the caret position at the same point relative to screen.
this._onUpdateCursor(
/* scroll */ updateCursor && weAreModifier,
/* zoom */ undefined,
/* keepCaretPositionRelativeToScreen */ !weAreModifier);
// Only for reference equality comparison.
this._lastVisibleCursorRef = app.file.textCursor.rectangle.clone();
},
_updateEditor: function(textMsg) {
textMsg = textMsg.substring('editor:'.length + 1);
var editorId = parseInt(textMsg);
var docLayer = this._map._docLayer;
docLayer._editorId = editorId;
if (docLayer._followEditor) {
docLayer._followThis = editorId;
}
if (this._map._viewInfo[editorId])
this._map.fire('updateEditorName', {username: this._map._viewInfo[editorId].username});
},
_onInvalidateViewCursorMsg: function (textMsg) {
var obj = JSON.parse(textMsg.substring('invalidateviewcursor:'.length + 1));
var viewId = parseInt(obj.viewId);
var docLayer = this._map._docLayer;
// Ignore if viewid is same as ours or not in our db
if (viewId === this._viewId || !this._map._viewInfo[viewId]) {
return;
}
var rectangle = this._getEditCursorRectangle(obj);
if (rectangle === undefined) {
return;
}
this._viewCursors[viewId] = this._viewCursors[viewId] || {};
this._viewCursors[viewId].bounds = new L.LatLngBounds(
this._twipsToLatLng(rectangle.getTopLeft(), this._map.getZoom()),
this._twipsToLatLng(rectangle.getBottomRight(), this._map.getZoom())),
this._viewCursors[viewId].corepxBounds = this._twipsToCorePixelsBounds(rectangle);
this._viewCursors[viewId].part = parseInt(obj.part);
this._viewCursors[viewId].mode = (obj.mode !== undefined) ? parseInt(obj.mode) : 0;
// FIXME. Server not sending view visible cursor
if (typeof this._viewCursors[viewId].visible === 'undefined') {
this._viewCursors[viewId].visible = true;
}
this._onUpdateViewCursor(viewId);
if (docLayer._followThis === viewId && (docLayer._followEditor || docLayer._followUser)) {
if (this._map.getDocType() === 'text' || this._map.getDocType() === 'presentation') {
this.goToViewCursor(viewId);
}
else if (this._map.getDocType() === 'spreadsheet') {
this.goToCellViewCursor(viewId);
}
}
this._saveMessageForReplay(textMsg, viewId);
},
_onCellViewCursorMsg: function (textMsg) {
var obj = JSON.parse(textMsg.substring('cellviewcursor:'.length + 1));
var viewId = parseInt(obj.viewId);
// Ignore if viewid is same as ours
if (viewId === this._viewId) {
return;
}
this._cellViewCursors[viewId] = this._cellViewCursors[viewId] || {};
if (!this._cellViewCursors[viewId].bounds) {
this._cellViewCursors[viewId].bounds = L.LatLngBounds.createDefault();
this._cellViewCursors[viewId].corePixelBounds = new L.Bounds();
}
if (obj.rectangle.match('EMPTY')) {
this._cellViewCursors[viewId].bounds = L.LatLngBounds.createDefault();
this._cellViewCursors[viewId].corePixelBounds = new L.Bounds();
}
else {
var strTwips = obj.rectangle.match(/\d+/g);
var topLeftTwips = new L.Point(parseInt(strTwips[0]), parseInt(strTwips[1]));
var offset = new L.Point(parseInt(strTwips[2]), parseInt(strTwips[3]));
var bottomRightTwips = topLeftTwips.add(offset);
var boundsTwips = this._convertToTileTwipsSheetArea(
new L.Bounds(topLeftTwips, bottomRightTwips));
this._cellViewCursors[viewId].bounds = new L.LatLngBounds(
this._twipsToLatLng(boundsTwips.getTopLeft(), this._map.getZoom()),
this._twipsToLatLng(boundsTwips.getBottomRight(), this._map.getZoom()));
var corePixelBounds = this._twipsToCorePixelsBounds(boundsTwips);
corePixelBounds.round();
this._cellViewCursors[viewId].corePixelBounds = corePixelBounds;
}
this._cellViewCursors[viewId].part = parseInt(obj.part);
this._onUpdateCellViewCursor(viewId);
if (this.isCalc()) {
this._saveMessageForReplay(textMsg, viewId);
}
},
_onUpdateCellViewCursor: function (viewId) {
if (!this._cellViewCursors[viewId] || !this._cellViewCursors[viewId].bounds)
return;
var cellViewCursorMarker = this._cellViewCursors[viewId].marker;
var viewPart = this._cellViewCursors[viewId].part;
var viewMode = this._cellViewCursors[viewId].mode ? this._cellViewCursors[viewId].mode : 0;
if (!this._isEmptyRectangle(this._cellViewCursors[viewId].bounds)
&& this._selectedPart === viewPart && this._selectedMode === viewMode
&& this._map.hasInfoForView(viewId)) {
if (!cellViewCursorMarker) {
var backgroundColor = L.LOUtil.rgbToHex(this._map.getViewColor(viewId));
cellViewCursorMarker = new CCellCursor(this._cellViewCursors[viewId].corePixelBounds, {
name: 'cell-view-cursor-' + viewId,
viewId: viewId,
fill: false,
color: backgroundColor,
weight: 2 * app.dpiScale,
toCompatUnits: function (corePx) {
return this._map.unproject(L.point(corePx)
.divideBy(app.dpiScale));
}.bind(this)
});
this._cellViewCursors[viewId].marker = cellViewCursorMarker;
cellViewCursorMarker.bindPopup(this._map.getViewName(viewId), {autoClose: false, autoPan: false, backgroundColor: backgroundColor, color: 'white', closeButton: false});
this._canvasOverlay.initPathGroup(cellViewCursorMarker);
}
else {
cellViewCursorMarker.setBounds(this._cellViewCursors[viewId].corePixelBounds);
}
}
else if (cellViewCursorMarker) {
this._canvasOverlay.removePathGroup(cellViewCursorMarker);
this._cellViewCursors[viewId].marker = undefined;
}
},
goToCellViewCursor: function(viewId) {
if (this._cellViewCursors[viewId] && !this._isEmptyRectangle(this._cellViewCursors[viewId].bounds)) {
if (!this._map.getBounds().contains(this._cellViewCursors[viewId].bounds)) {
var mapBounds = this._map.getBounds();
var scrollX = 0;
var scrollY = 0;
var spacingX = Math.abs(this._cellViewCursors[viewId].bounds.getEast() - this._cellViewCursors[viewId].bounds.getWest()) / 4.0;
var spacingY = Math.abs(this._cellViewCursors[viewId].bounds.getSouth() - this._cellViewCursors[viewId].bounds.getNorth()) / 4.0;
if (this._cellViewCursors[viewId].bounds.getWest() < mapBounds.getWest()) {
scrollX = this._cellViewCursors[viewId].bounds.getWest() - mapBounds.getWest() - spacingX;
} else if (this._cellViewCursors[viewId].bounds.getEast() > mapBounds.getEast()) {
scrollX = this._cellViewCursors[viewId].bounds.getEast() - mapBounds.getEast() + spacingX;
}
if (this._cellViewCursors[viewId].bounds.getNorth() > mapBounds.getNorth()) {
scrollY = this._cellViewCursors[viewId].bounds.getNorth() - mapBounds.getNorth() + spacingY;
} else if (this._cellViewCursors[viewId].bounds.getSouth() < mapBounds.getSouth()) {
scrollY = this._cellViewCursors[viewId].bounds.getSouth() - mapBounds.getSouth() - spacingY;
}
if (scrollX !== 0 || scrollY !== 0) {
var newCenter = mapBounds.getCenter();
newCenter.lat += scrollX;
newCenter.lat += scrollY;
this.scrollToPos(newCenter);
}
}
var backgroundColor = L.LOUtil.rgbToHex(this._map.getViewColor(viewId));
this._cellViewCursors[viewId].marker.bindPopup(this._map.getViewName(viewId), {autoClose: false, autoPan: false, backgroundColor: backgroundColor, color: 'white', closeButton: false});
}
},
_onViewCursorVisibleMsg: function(textMsg) {
textMsg = textMsg.substring('viewcursorvisible:'.length + 1);
var obj = JSON.parse(textMsg);
var viewId = parseInt(obj.viewId);
// Ignore if viewid is same as ours or not in our db
if (viewId === this._viewId || !this._map._viewInfo[viewId]) {
return;
}
if (typeof this._viewCursors[viewId] !== 'undefined') {
this._viewCursors[viewId].visible = (obj.visible === 'true');
}
this._onUpdateViewCursor(viewId);
},
_addView: function(viewInfo) {
if (viewInfo.color === 0 && this._map.getDocType() !== 'text') {
viewInfo.color = L.LOUtil.getViewIdColor(viewInfo.id);
}
this._map.addView(viewInfo);
//TODO: We can initialize color and other properties here.
if (typeof this._viewCursors[viewInfo.id] !== 'undefined') {
this._viewCursors[viewInfo.id] = {};
}
this._onUpdateViewCursor(viewInfo.id);
},
_removeView: function(viewId) {
// Remove selection, if any.
if (this._viewSelections[viewId]) {
if (this._viewSelections[viewId].selection) {
this._viewSelections[viewId].selection.remove();
this._viewSelections[viewId].selection = undefined;
}
delete this._viewSelections[viewId];
}
// update viewcursor in writer
if (typeof this._viewCursors[viewId] !== 'undefined') {
this._viewCursors[viewId].visible = false;
this._onUpdateViewCursor(viewId);
delete this._viewCursors[viewId];
}
// update cellviewcursor in calc
if (typeof this._cellViewCursors[viewId] !== 'undefined') {
this._cellViewCursors[viewId].bounds = L.LatLngBounds.createDefault();
this._cellViewCursors[viewId].corePixelBounds = new L.Bounds();
this._onUpdateCellViewCursor(viewId);
delete this._cellViewCursors[viewId];
}
this._map.removeView(viewId);
},
removeAllViews: function() {
for (var viewInfoIdx in this._map._viewInfo) {
this._removeView(parseInt(viewInfoIdx));
}
},
_onViewInfoMsg: function(textMsg) {
textMsg = textMsg.substring('viewinfo: '.length);
var viewInfo = JSON.parse(textMsg);
this._map.fire('viewinfo', viewInfo);
// A new view
var viewIds = [];
for (var viewInfoIdx in viewInfo) {
if (!(parseInt(viewInfo[viewInfoIdx].id) in this._map._viewInfo)) {
this._addView(viewInfo[viewInfoIdx]);
}
viewIds.push(viewInfo[viewInfoIdx].id);
}
// Check if any view is deleted
for (viewInfoIdx in this._map._viewInfo) {
if (viewIds.indexOf(parseInt(viewInfoIdx)) === -1) {
this._removeView(parseInt(viewInfoIdx));
}
}
// Sending postMessage about View_Added / View_Removed is
// deprecated, going forward we prefer sending the entire information.
this._map.fire('updateviewslist');
},
_onRenderFontMsg: function (textMsg, img) {
var command = app.socket.parseServerCmd(textMsg);
this._map.fire('renderfont', {
font: command.font,
char: command.char,
img: img
});
},
_onSearchNotFoundMsg: function (textMsg) {
this._clearSearchResults();
this._searchRequested = false;
var originalPhrase = textMsg.substring(16);
this._map.fire('search', {originalPhrase: originalPhrase, count: 0});
},
_getSearchResultRectangles: function (obj, results) {
for (var i = 0; i < obj.searchResultSelection.length; i++) {
results.push({
part: parseInt(obj.searchResultSelection[i].part),
rectangles: this._twipsRectanglesToPixelBounds(obj.searchResultSelection[i].rectangles),
twipsRectangles: obj.searchResultSelection[i].rectangles
});
}
},
_getSearchResultRectanglesFileBasedView: function (obj, results) {
var additionPerPart = this._partHeightTwips + this._spaceBetweenParts;
for (var i = 0; i < obj.searchResultSelection.length; i++) {
var rectangles = obj.searchResultSelection[i].rectangles;
var part = parseInt(obj.searchResultSelection[i].part);
rectangles = rectangles.split(',');
rectangles = rectangles.map(function(element, index) {
element = parseInt(element);
if (index < 2)
element += additionPerPart * part;
return element;
});
rectangles = String(rectangles[0]) + ', ' + String(rectangles[1]) + ', ' + String(rectangles[2]) + ', ' + String(rectangles[3]);
results.push({
part: parseInt(obj.searchResultSelection[i].part),
rectangles: this._twipsRectanglesToPixelBounds(rectangles),
twipsRectangles: rectangles
});
}
},
_onSearchResultSelection: function (textMsg) {
this._searchRequested = false;
textMsg = textMsg.substring(23);
var obj = JSON.parse(textMsg);
var originalPhrase = obj.searchString;
var count = obj.searchResultSelection.length;
var highlightAll = obj.highlightAll;
var results = [];
if (!app.file.fileBasedView)
this._getSearchResultRectangles(obj, results);
else
this._getSearchResultRectanglesFileBasedView(obj, results);
// do not cache search results if there is only one result.
// this way regular searches works fine
if (count > 1)
{
this._clearSearchResults();
this._searchResults = results;
if (!app.file.fileBasedView)
this._map.setPart(results[0].part); // go to first result.
else
this._map._docLayer._preview._scrollViewToPartPosition(results[0].part);
} else if (count === 1) {
this._lastSearchResult = results[0];
}
this._searchTerm = originalPhrase;
this._map.fire('search', {originalPhrase: originalPhrase, count: count, highlightAll: highlightAll, results: results});
},
_clearSearchResults: function() {
if (this._searchTerm) {
this._textCSelections.clear();
}
this._lastSearchResult = null;
this._searchResults = null;
this._searchTerm = null;
this._searchResultsLayer.clearLayers();
},
_drawSearchResults: function() {
if (!this._searchResults) {
return;
}
this._searchResultsLayer.clearLayers();
for (var k = 0; k < this._searchResults.length; k++)
{
var result = this._searchResults[k];
if (result.part === this._selectedPart)
{
var _fillColor = '#CCCCCC';
var strTwips = result.twipsRectangles.match(/\d+/g);
var rectangles = [];
for (var i = 0; i < strTwips.length; i += 4) {
var topLeftTwips = new L.Point(parseInt(strTwips[i]), parseInt(strTwips[i + 1]));
var offset = new L.Point(parseInt(strTwips[i + 2]), parseInt(strTwips[i + 3]));
var topRightTwips = topLeftTwips.add(new L.Point(offset.x, 0));
var bottomLeftTwips = topLeftTwips.add(new L.Point(0, offset.y));
var bottomRightTwips = topLeftTwips.add(offset);
rectangles.push([bottomLeftTwips, bottomRightTwips, topLeftTwips, topRightTwips]);
}
var polygons = L.PolyUtil.rectanglesToPolygons(rectangles, this);
var selection = new L.Polygon(polygons, {
pointerEvents: 'none',
fillColor: _fillColor,
fillOpacity: 0.25,
weight: 2,
opacity: 0.25});
this._searchResultsLayer.addLayer(selection);
}
}
},
_onStateChangedMsg: function (textMsg) {
textMsg = textMsg.substr(14);
var isPureJSON = textMsg.indexOf('=') === -1 && textMsg.indexOf('{') !== -1;
if (isPureJSON) {
var json = JSON.parse(textMsg);
// json.state as empty string is fine, for example it means no selection
// when json.commandName is '.uno:RowColSelCount'.
if (json.commandName && json.state !== undefined) {
this._map.fire('commandstatechanged', json);
}
} else {
var index = textMsg.indexOf('=');
var commandName = index !== -1 ? textMsg.substr(0, index) : '';
var state = index !== -1 ? textMsg.substr(index + 1) : '';
this._map.fire('commandstatechanged', {commandName : commandName, state : state});
}
},
_onUnoCommandResultMsg: function (textMsg) {
// window.app.console.log('_onUnoCommandResultMsg: "' + textMsg + '"');
textMsg = textMsg.substring(18);
var obj = JSON.parse(textMsg);
var commandName = obj.commandName;
if (obj.success === 'true' || obj.success === true) {
var success = true;
}
else if (obj.success === 'false' || obj.success === false) {
success = false;
}
this._map.hideBusy();
this._map.fire('commandresult', {commandName: commandName, success: success, result: obj.result});
if (this._map.CallPythonScriptSource != null) {
this._map.CallPythonScriptSource.postMessage(JSON.stringify({'MessageId': 'CallPythonScript-Result',
'SendTime': Date.now(),
'Values': obj
}),
'*');
this._map.CallPythonScriptSource = null;
}
},
_onRulerUpdate: function (textMsg) {
textMsg = textMsg.substring(13);
var obj = JSON.parse(textMsg);
this._map.fire('rulerupdate', obj);
},
_onContextMenuMsg: function (textMsg) {
textMsg = textMsg.substring(13);
var obj = JSON.parse(textMsg);
this._map.fire('locontextmenu', obj);
},
_onTextSelectionMsg: function (textMsg) {
var rectArray = this._getTextSelectionRectangles(textMsg);
var inTextSearch = $('input#search-input').is(':focus');
var isTextSelection = this.isCursorVisible() || inTextSearch;
if (rectArray.length) {
var rectangles = rectArray.map(function (rect) {
return rect.getPointArray();
});
if (app.file.fileBasedView && this._lastSearchResult) {
// We rely on that _lastSearchResult has been updated before this function is called.
var additionPerPart = this._partHeightTwips + this._spaceBetweenParts;
for (var i = 0; i < rectangles.length; i++) {
for (var j = 0; j < rectangles[i].length; j++) {
rectangles[i][j].y += additionPerPart * this._lastSearchResult.part;
}
}
this._map._docLayer._preview._scrollViewToPartPosition(this._lastSearchResult.part);
this._updateFileBasedView();
setTimeout(function () {app.sectionContainer.requestReDraw();}, 100);
}
var docLayer = this;
var pointSet = CPolyUtil.rectanglesToPointSet(rectangles,
function (twipsPoint) {
var corePxPt = docLayer._twipsToCorePixels(twipsPoint);
corePxPt.round();
return corePxPt;
});
if (isTextSelection)
this._textCSelections.setPointSet(pointSet);
else
this._cellCSelections.setPointSet(pointSet);
this._map.removeLayer(this._map._textInput._cursorHandler); // User selected a text, we remove the carret marker.
if (L.Browser.hasNavigatorClipboardWrite) {
// Just set the selection type, no fetch of the content.
this._map._clip.setTextSelectionType('text');
} else {
// Trigger fetching the selection content, we already need to have
// it locally by the time 'copy' is executed.
if (this._selectionContentRequest) {
clearTimeout(this._selectionContentRequest);
}
this._selectionContentRequest = setTimeout(L.bind(function () {
app.socket.sendMessage('gettextselection mimetype=text/html,text/plain;charset=utf-8');}, this), 100);
}
}
else {
this._textCSelections.clear();
this._cellCSelections.clear();
if (this._map._clip && this._map._clip._selectionType === 'complex')
this._map._clip.clearSelection();
}
this._onUpdateTextSelection();
},
_onTextViewSelectionMsg: function (textMsg) {
var obj = JSON.parse(textMsg.substring('textviewselection:'.length + 1));
var viewId = parseInt(obj.viewId);
var viewPart = parseInt(obj.part);
var viewMode = (obj.mode !== undefined) ? parseInt(obj.mode) : 0;
// Ignore if viewid is same as ours or not in our db
if (viewId === this._viewId || !this._map._viewInfo[viewId]) {
return;
}
var rectArray = this._getTextSelectionRectangles(obj.selection);
this._viewSelections[viewId] = this._viewSelections[viewId] || {};
if (rectArray.length) {
var rectangles = rectArray.map(function (rect) {
return rect.getPointArray();
});
this._viewSelections[viewId].part = viewPart;
this._viewSelections[viewId].mode = viewMode;
var docLayer = this;
this._viewSelections[viewId].pointSet = CPolyUtil.rectanglesToPointSet(rectangles,
function (twipsPoint) {
var corePxPt = docLayer._twipsToCorePixels(twipsPoint);
corePxPt.round();
return corePxPt;
});
} else {
this._viewSelections[viewId].pointSet = new CPointSet();
}
this._onUpdateTextViewSelection(viewId);
this._saveMessageForReplay(textMsg, viewId);
},
_updateReferenceMarks: function() {
this._clearReferences();
if (!this._referencesAll)
return;
for (var i = 0; i < this._referencesAll.length; i++) {
// Avoid doubled marks, add only marks for current sheet
if (!this._references.hasMark(this._referencesAll[i].mark)
&& this._selectedPart === this._referencesAll[i].part) {
this._references.addMark(this._referencesAll[i].mark);
}
if (!window.mode.isDesktop()) {
if (!this._referenceMarkerStart.isDragged) {
this._map.addLayer(this._referenceMarkerStart);
var sizeStart = this._referenceMarkerStart._icon.getBoundingClientRect();
var posStart = this._referencesAll[i].mark.getBounds().getTopLeft().divideBy(app.dpiScale);
posStart = posStart.subtract(new L.Point(sizeStart.width / 2, sizeStart.height / 2));
posStart = this._map.unproject(posStart);
this._referenceMarkerStart.setLatLng(posStart);
}
if (!this._referenceMarkerEnd.isDragged) {
this._map.addLayer(this._referenceMarkerEnd);
var sizeEnd = this._referenceMarkerEnd._icon.getBoundingClientRect();
var posEnd = this._referencesAll[i].mark.getBounds().getBottomRight().divideBy(app.dpiScale);
posEnd = posEnd.subtract(new L.Point(sizeEnd.width / 2, sizeEnd.height / 2));
posEnd = this._map.unproject(posEnd);
this._referenceMarkerEnd.setLatLng(posEnd);
}
}
}
},
_onReferencesMsg: function (textMsg) {
textMsg = textMsg.substr(textMsg.indexOf(' ') + 1);
var marks = JSON.parse(textMsg);
marks = marks.marks;
var references = [];
this._referencesAll = [];
for (var mark = 0; mark < marks.length; mark++) {
var strTwips = marks[mark].rectangle.match(/\d+/g);
var strColor = marks[mark].color;
var part = parseInt(marks[mark].part);
if (strTwips != null) {
var rectangles = [];
for (var i = 0; i < strTwips.length; i += 4) {
var topLeftTwips = new L.Point(parseInt(strTwips[i]), parseInt(strTwips[i + 1]));
var offset = new L.Point(parseInt(strTwips[i + 2]), parseInt(strTwips[i + 3]));
var boundsTwips = this._convertToTileTwipsSheetArea(
new L.Bounds(topLeftTwips, topLeftTwips.add(offset)));
rectangles.push([boundsTwips.getBottomLeft(), boundsTwips.getBottomRight(),
boundsTwips.getTopLeft(), boundsTwips.getTopRight()]);
}
var docLayer = this;
var pointSet = CPolyUtil.rectanglesToPointSet(rectangles, function (twipsPoint) {
var corePxPt = docLayer._twipsToCorePixels(twipsPoint);
corePxPt.round();
return corePxPt;
});
var reference = new CPolygon(pointSet, {
pointerEvents: 'none',
fillColor: '#' + strColor,
fillOpacity: 0.25,
weight: 2 * app.dpiScale,
opacity: 0.25});
references.push({mark: reference, part: part});
}
}
for (i = 0; i < references.length; i++) {
this._referencesAll.push(references[i]);
}
this._updateReferenceMarks();
},
_getStringPart: function (string) {
var code = '';
var i = 0;
while (i < string.length) {
if (string.charCodeAt(i) < 48 || string.charCodeAt(i) > 57) {
code += string.charAt(i);
}
i++;
}
return code;
},
_getNumberPart: function (string) {
var number = '';
var i = 0;
while (i < string.length) {
if (string.charCodeAt(i) >= 48 && string.charCodeAt(i) <= 57) {
number += string.charAt(i);
}
i++;
}
return parseInt(number);
},
_isWholeColumnSelected: function (cellAddress) {
if (!cellAddress)
cellAddress = document.getElementById('addressInput-input').value;
var startEnd = cellAddress.split(':');
if (startEnd.length === 1)
return false; // Selection is not a range.
var rangeStart = this._getNumberPart(startEnd[0]);
if (rangeStart !== 1)
return false; // Selection doesn't start at first row.
var rangeEnd = this._getNumberPart(startEnd[1]);
if (rangeEnd === 1048576) // Last row's number.
return true;
else
return false;
},
_isWholeRowSelected: function (cellAddress) {
if (!cellAddress)
cellAddress = document.getElementById('addressInput-input').value;
var startEnd = cellAddress.split(':');
if (startEnd.length === 1)
return false; // Selection is not a range.
var rangeStart = this._getStringPart(startEnd[0]);
if (rangeStart !== 'A')
return false; // Selection doesn't start at first column.
var rangeEnd = this._getStringPart(startEnd[1]);
if (rangeEnd === 'XFD') // Last column's code.
return true;
else
return false;
},
_latLngBoundsToSimpleRectangle: function(latLngBounds) {
let topLeft = this._latLngToTwips(latLngBounds.getNorthWest());
let bottomRight = this._latLngToTwips(latLngBounds.getSouthEast());
return new app.definitions.simpleRectangle(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
},
_simpleRectangleToLatLngBounds: function(simpleRectangle) {
return new L.LatLngBounds(
this._twipsToLatLng({ x: simpleRectangle.x1, y: simpleRectangle.y1 }, this._map.getZoom()),
this._twipsToLatLng({ x: simpleRectangle.x2, y: simpleRectangle.y2 }, this._map.getZoom()));
},
_updateScrollOnCellSelection: function (oldSelection, newSelection) {
if (this.isCalc() && oldSelection) {
if (oldSelection._northEast) // Is object's type latLngBounds
oldSelection = this._latLngBoundsToSimpleRectangle(oldSelection);
if (newSelection._northEast) // Is object's type latLngBounds
newSelection = this._latLngBoundsToSimpleRectangle(newSelection);
if (!app.file.viewedRectangle.containsRectangle(newSelection.toArray()) && !newSelection.equals(oldSelection.toArray())) {
var spacingX = Math.abs(app.calc.cellCursorRectangle.pWidth) / 4.0;
var spacingY = Math.abs(app.calc.cellCursorRectangle.pHeight) / 2.0;
var scrollX = 0, scrollY = 0;
if (newSelection.pX2 > app.file.viewedRectangle.pX2 && newSelection.pX2 > oldSelection.pX2)
scrollX = newSelection.pX2 - app.file.viewedRectangle.pX2 + spacingX;
else if (newSelection.pX1 < app.file.viewedRectangle.pX1 && newSelection.pX1 < oldSelection.pX1)
scrollX = newSelection.pX1 - app.file.viewedRectangle.pX1 - spacingX;
if (newSelection.pY1 > app.file.viewedRectangle.pY1 && newSelection.pY1 > oldSelection.pY1)
scrollY = newSelection.pY1 - app.file.viewedRectangle.pY1 + spacingY;
else if (newSelection.pY2 < app.file.viewedRectangle.pY2 && newSelection.pY2 < oldSelection.pY2)
scrollY = newSelection.pY2 - app.file.viewedRectangle.pY2 - spacingY;
if (scrollX !== 0 || scrollY !== 0) {
var newCenter = new app.definitions.simplePoint(app.file.viewedRectangle.center[0], app.file.viewedRectangle.center[1]);
newCenter.pX += scrollX;
newCenter.pY += scrollY;
if (!this._map.wholeColumnSelected && !this._map.wholeRowSelected) {
var address = document.getElementById('addressInput-input').value;
if (!this._isWholeColumnSelected(address) && !this._isWholeRowSelected(address))
this.scrollToPos(newCenter);
}
}
}
}
},
_onTextSelectionEndMsg: function (textMsg) {
var rectangles = this._getTextSelectionRectangles(textMsg);
if (rectangles.length) {
var topLeftTwips = rectangles[0].getTopLeft();
var bottomRightTwips = rectangles[0].getBottomRight();
var oldSelection = this._textSelectionEnd;
this._textSelectionEnd = new L.LatLngBounds(
this._twipsToLatLng(topLeftTwips, this._map.getZoom()),
this._twipsToLatLng(bottomRightTwips, this._map.getZoom()));
this._updateScrollOnCellSelection(oldSelection, this._textSelectionEnd);
this._updateMarkers();
}
else {
this._textSelectionEnd = null;
}
},
_onTextSelectionStartMsg: function (textMsg) {
var rectangles = this._getTextSelectionRectangles(textMsg);
if (rectangles.length) {
var topLeftTwips = rectangles[0].getTopLeft();
var bottomRightTwips = rectangles[0].getBottomRight();
var oldSelection = this._textSelectionStart;
//FIXME: The selection is really not two points, as they can be
//FIXME: on top of each other, but on separate lines. We should
//FIXME: capture the whole area in _onTextSelectionMsg.
this._textSelectionStart = new L.LatLngBounds(
this._twipsToLatLng(topLeftTwips, this._map.getZoom()),
this._twipsToLatLng(bottomRightTwips, this._map.getZoom()));
this._updateScrollOnCellSelection(oldSelection, this._textSelectionStart);
}
else {
this._textSelectionStart = null;
}
},
_refreshRowColumnHeaders: function () {
if (app.sectionContainer.doesSectionExist(L.CSections.RowHeader.name))
app.sectionContainer.getSectionWithName(L.CSections.RowHeader.name)._updateCanvas();
if (app.sectionContainer.doesSectionExist(L.CSections.ColumnHeader.name))
app.sectionContainer.getSectionWithName(L.CSections.ColumnHeader.name)._updateCanvas();
},
_onCellSelectionAreaMsg: function (textMsg) {
var autofillMarkerSection = app.sectionContainer.getSectionWithName(L.CSections.AutoFillMarker.name);
var strTwips = textMsg.match(/\d+/g);
if (strTwips != null) {
var topLeftTwips = new L.Point(parseInt(strTwips[0]), parseInt(strTwips[1]));
var offset = new L.Point(parseInt(strTwips[2]), parseInt(strTwips[3]));
var bottomRightTwips = topLeftTwips.add(offset);
var boundsTwips = this._convertToTileTwipsSheetArea(
new L.Bounds(topLeftTwips, bottomRightTwips));
var oldSelection = this._cellSelectionArea;
this._cellSelectionArea = new L.LatLngBounds(
this._twipsToLatLng(boundsTwips.getTopLeft(), this._map.getZoom()),
this._twipsToLatLng(boundsTwips.getBottomRight(), this._map.getZoom()));
var offsetPixels = this._twipsToCorePixels(boundsTwips.getSize());
var start = this._twipsToCorePixels(boundsTwips.min);
var cellSelectionAreaPixels = L.LOUtil.createRectangle(start.x, start.y, offsetPixels.x, offsetPixels.y);
if (autofillMarkerSection)
autofillMarkerSection.calculatePositionViaCellSelection([cellSelectionAreaPixels.getX2(), cellSelectionAreaPixels.getY2()]);
this._updateScrollOnCellSelection(oldSelection, this._cellSelectionArea);
} else {
this._cellSelectionArea = null;
if (autofillMarkerSection)
autofillMarkerSection.calculatePositionViaCellSelection(null);
this._cellSelections = Array(0);
this._map.wholeColumnSelected = false; // Message related to whole column/row selection should be on the way, we should update the variables now.
this._map.wholeRowSelected = false;
if (this._refreshRowColumnHeaders)
this._refreshRowColumnHeaders();
}
},
_onCellAutoFillAreaMsg: function (textMsg) {
var strTwips = textMsg.match(/\d+/g);
if (strTwips != null && this._map.isEditMode()) {
var topLeftTwips = new L.Point(parseInt(strTwips[0]), parseInt(strTwips[1]));
var offset = new L.Point(parseInt(strTwips[2]), parseInt(strTwips[3]));
var topLeftPixels = this._twipsToCorePixels(topLeftTwips);
var offsetPixels = this._twipsToCorePixels(offset);
this._cellAutoFillAreaPixels = L.LOUtil.createRectangle(topLeftPixels.x, topLeftPixels.y, offsetPixels.x, offsetPixels.y);
}
else {
this._cellAutoFillAreaPixels = null;
}
},
_onDialogPaintMsg: function(textMsg, img) {
var command = app.socket.parseServerCmd(textMsg);
// app.socket.sendMessage('DEBUG _onDialogPaintMsg: hash=' + command.hash + ' img=' + typeof(img) + (typeof(img) == 'string' ? (' (length:' + img.length + ':"' + img.substring(0, 30) + (img.length > 30 ? '...' : '') + '")') : '') + ', cache size ' + this._pngCache.length);
if (command.nopng) {
var found = false;
for (var i = 0; i < this._pngCache.length; i++) {
if (this._pngCache[i].hash == command.hash) {
found = true;
// app.socket.sendMessage('DEBUG - Found in cache');
img = this._pngCache[i].img;
// Remove item (and add it below at the start of the array)
this._pngCache.splice(i, 1);
break;
}
}
if (!found) {
var message = 'windowpaint: message assumed PNG for hash ' + command.hash
+ ' is cached here in the client but not found';
if (L.Browser.cypressTest)
throw new Error(message);
app.socket.sendMessage('ERROR ' + message);
// Not sure what to do. Ask the server to re-send the windowpaint: message but this time including the PNG?
}
} else {
// Sanity check: If we get a PNG it should be for a hash that we don't have cached
for (i = 0; i < this._pngCache.length; i++) {
if (this._pngCache[i].hash == command.hash) {
message = 'windowpaint: message included PNG for hash ' + command.hash
+ ' even if it was already cached here in the client';
if (L.Browser.cypressTest)
throw new Error(message);
app.socket.sendMessage('ERROR ' + message);
// Remove the extra copy, code below will add it at the start of the array
this._pngCache.splice(i, 1);
break;
}
}
}
// If cache is max size, drop the last element
if (this._pngCache.length == app.socket.TunnelledDialogImageCacheSize) {
// app.socket.sendMessage('DEBUG - Dropping last cache element');
this._pngCache.pop();
}
// Add element to cache
this._pngCache.unshift({hash: command.hash, img:img});
// app.socket.sendMessage('DEBUG - Cache size now ' + this._pngCache.length);
this._map.fire('windowpaint', {
id: command.id,
img: img,
width: command.width,
height: command.height,
rectangle: command.rectangle,
hash: command.hash
});
},
_onDialogMsg: function(textMsg) {
textMsg = textMsg.substring('window: '.length);
var dialogMsg = JSON.parse(textMsg);
// e.type refers to signal type
dialogMsg.winType = dialogMsg.type;
this._map.fire('window', dialogMsg);
},
_mapOnError: function (e) {
if (e.msg && this._map.isEditMode() && e.critical !== false) {
this._map.setPermission('view');
}
},
_clearSelections: function (calledFromSetPartHandler) {
// hide the cursor if not editable
this._onUpdateCursor(calledFromSetPartHandler);
// hide the text selection
this._textCSelections.clear();
// hide the cell selection
this._cellCSelections.clear();
// hide the ole selection
this._oleCSelections.clear();
// hide the selection handles
this._onUpdateTextSelection();
// hide the graphic selection
this._graphicSelection = new L.LatLngBounds(new L.LatLng(0, 0), new L.LatLng(0, 0));
this._onUpdateGraphicSelection();
app.calc.cellCursorVisible = false;
this._prevCellCursor = null;
this._onUpdateCellCursor();
if (this._map._clip)
this._map._clip.clearSelection();
else
this._selectedTextContent = '';
},
containsSelection: function (latlng) {
var corepxPoint = this._map.project(latlng);
return this._textCSelections.empty() ?
this._cellCSelections.contains(corepxPoint) :
this._textCSelections.contains(corepxPoint);
},
_clearReferences: function () {
this._references.clear();
if (!this._referenceMarkerStart.isDragged)
this._map.removeLayer(this._referenceMarkerStart);
if (!this._referenceMarkerEnd.isDragged)
this._map.removeLayer(this._referenceMarkerEnd);
},
_postMouseEvent: function(type, x, y, count, buttons, modifier) {
if (!this._map._docLoaded)
return;
if (this._map.calcInputBarHasFocus() && type === 'move') {
// When the Formula-bar has the focus, sending
// mouse move with the document coordinates
// hides the cursor (lost focus?). This is clearly
// a bug in Core, but we need to work around it
// until fixed. Just don't send mouse move.
return;
}
this._sendClientZoom();
this._sendClientVisibleArea();
app.socket.sendMessage('mouse type=' + type +
' x=' + x + ' y=' + y + ' count=' + count +
' buttons=' + buttons + ' modifier=' + modifier);
if (type === 'buttondown') {
this._clearSearchResults();
}
},
// Given a character code and a UNO keycode, send a "key" message to coolwsd.
//
// "type" is either "input" for key presses (akin to the DOM "keypress"
// / "beforeinput" events) and "up" for key releases (akin to the DOM
// "keyup" event).
//
// PageUp/PageDown and select column & row are handled as special cases for spreadsheets - in
// addition of sending messages to coolwsd, they move the cell cursor around.
postKeyboardEvent: function(type, charCode, unoKeyCode) {
if (!this._map._docLoaded)
return;
if (L.Browser.mac) {
// Map Mac standard shortcuts to the LO shortcuts for the corresponding
// functions when possible. Note that the Cmd modifier comes here as CTRL.
// Cmd+UpArrow -> Ctrl+Home
if (unoKeyCode == UNOKey.UP + UNOModifier.CTRL)
unoKeyCode = UNOKey.HOME + UNOModifier.CTRL;
// Cmd+DownArrow -> Ctrl+End
else if (unoKeyCode == UNOKey.DOWN + UNOModifier.CTRL)
unoKeyCode = UNOKey.END + UNOModifier.CTRL;
// Cmd+LeftArrow -> Home
else if (unoKeyCode == UNOKey.LEFT + UNOModifier.CTRL)
unoKeyCode = UNOKey.HOME;
// Cmd+RightArrow -> End
else if (unoKeyCode == UNOKey.RIGHT + UNOModifier.CTRL)
unoKeyCode = UNOKey.END;
// Option+LeftArrow -> Ctrl+LeftArrow
else if (unoKeyCode == UNOKey.LEFT + UNOModifier.ALT)
unoKeyCode = UNOKey.LEFT + UNOModifier.CTRL;
// Option+RightArrow -> Ctrl+RightArrow (Not entirely equivalent, should go
// to end of word (or next), LO goes to beginning of next word.)
else if (unoKeyCode == UNOKey.RIGHT + UNOModifier.ALT)
unoKeyCode = UNOKey.RIGHT + UNOModifier.CTRL;
}
var completeEvent = app.socket.createCompleteTraceEvent('L.TileSectionManager.postKeyboardEvent', { type: type, charCode: charCode });
var winId = this._map.getWinId();
if (
this.isCalc() &&
this._prevCellCursor &&
type === 'input' &&
winId === 0
) {
if (unoKeyCode === UNOKey.PAGEUP) {
if (this._cellCursorOnPgUp) {
return;
}
this._cellCursorOnPgUp = this._prevCellCursor.clone();
}
else if (unoKeyCode === UNOKey.PAGEDOWN) {
if (this._cellCursorOnPgDn) {
return;
}
this._cellCursorOnPgDn = this._prevCellCursor.clone();
}
else if (unoKeyCode === UNOKey.SPACE + UNOModifier.CTRL) { // Select whole column.
this._map.wholeColumnSelected = true;
}
else if (unoKeyCode === UNOKey.SPACE + UNOModifier.SHIFT) { // Select whole row.
this._map.wholeRowSelected = true;
}
}
this._sendClientZoom();
this._sendClientVisibleArea();
if (winId === 0) {
app.socket.sendMessage(
'key' +
' type=' + type +
' char=' + charCode +
' key=' + unoKeyCode +
'\n'
);
} else {
app.socket.sendMessage(
'windowkey id=' + winId +
' type=' + type +
' char=' + charCode +
' key=' + unoKeyCode +
'\n'
);
}
if (completeEvent)
completeEvent.finish();
},
_postSelectTextEvent: function(type, x, y) {
app.socket.sendMessage('selecttext type=' + type +
' x=' + x + ' y=' + y);
},
// Is rRectangle empty?
_isEmptyRectangle: function (bounds) {
if (!bounds) {
return true;
}
return bounds.getSouthWest().equals(new L.LatLng(0, 0)) && bounds.getNorthEast().equals(new L.LatLng(0, 0));
},
_onZoomStart: function () {
this._isZooming = true;
},
_onZoomEnd: function () {
this._isZooming = false;
if (!this.isCalc())
this._replayPrintTwipsMsgs(false);
this._onUpdateCursor(null, true);
this.updateAllViewCursors();
},
_updateCursorPos: function () {
var cursorPos = new L.Point(app.file.textCursor.rectangle.pX1, app.file.textCursor.rectangle.pY1);
var cursorSize = new L.Point(app.file.textCursor.rectangle.pWidth, app.file.textCursor.rectangle.pHeight);
if (!this._cursorMarker) {
this._cursorMarker = new Cursor(cursorPos, cursorSize, this._map, { blink: true });
} else {
this._cursorMarker.setPositionSize(cursorPos, cursorSize);
}
},
goToTarget: function(target) {
var command = {
'Name': {
type: 'string',
value: 'URL'
},
'URL': {
type: 'string',
value: '#' + target
}
};
this._map.sendUnoCommand('.uno:OpenHyperlink', command);
},
_allowViewJump: function() {
return (!this._map._clip || this._map._clip._selectionType !== 'complex') &&
!this._referenceMarkerStart.isDragged && !this._referenceMarkerEnd.isDragged;
},
// Scrolls the view to selected position
scrollToPos: function(pos) {
if (pos.pX) // Turn into lat/lng if required (pos may also be a simplePoint.).
pos = this._twipsToLatLng({ x: pos.x, y: pos.y });
var center = this._map.project(pos);
center = center.subtract(this._map.getSize().divideBy(2));
center.x = Math.round(center.x < 0 ? 0 : center.x);
center.y = Math.round(center.y < 0 ? 0 : center.y);
this._map.fire('scrollto', {x: center.x, y: center.y});
},
// Update cursor layer (blinking cursor).
_onUpdateCursor: function (scroll, zoom, keepCaretPositionRelativeToScreen) {
if (!app.file.textCursor.visible ||
this._referenceMarkerStart.isDragged ||
this._referenceMarkerEnd.isDragged ||
this._map.ignoreCursorUpdate()) {
return;
}
var docLayer = this._map._docLayer;
if (!zoom
&& scroll !== false
&& (app.file.textCursor.visible
|| (this._graphicSelection && !this._isEmptyRectangle(this._graphicSelection)))
// Do not center view in Calc if no new cursor coordinates have arrived yet.
// ie, 'invalidatecursor' has not arrived after 'cursorvisible' yet.
&& (!this.isCalc() || (this._lastVisibleCursorRef && !this._lastVisibleCursorRef.equals(app.file.textCursor.rectangle.toArray())))
&& this._allowViewJump()) {
// Cursor invalidation should take most precedence among all the scrolling to follow the cursor
// so here we disregard all the pending scrolling
this._map._docLayer._painter._sectionContainer.getSectionWithName(L.CSections.Scroll.name).pendingScrollEvent = null;
var correctedCursor = app.file.textCursor.rectangle.clone();
if (this._docType === 'text') {
// For Writer documents, disallow scrolling to cursor outside of the page (horizontally)
// Use document dimensions to approximate page width
correctedCursor.x1 = clamp(correctedCursor.x1, 0, app.file.size.twips[0]);
correctedCursor.x2 = clamp(correctedCursor.x2, 0, app.file.size.twips[0]);
}
if (!app.isPointVisibleInTheDisplayedArea(new app.definitions.simplePoint(correctedCursor.x1, correctedCursor.y1).toArray()) ||
!app.isPointVisibleInTheDisplayedArea(new app.definitions.simplePoint(correctedCursor.x2, correctedCursor.y2).toArray())) {
if (!(this._selectionHandles.start && this._selectionHandles.start.isDragged) &&
!(this._selectionHandles.end && this._selectionHandles.end.isDragged) &&
!(docLayer._followEditor || docLayer._followUser) &&
!this._map.calcInputBarHasFocus()) {
this.scrollToPos(new app.definitions.simplePoint(correctedCursor.x1, correctedCursor.y1));
}
}
}
else if (keepCaretPositionRelativeToScreen) {
/* We should be here when:
Another view updated the text.
That edit changed our cursor position.
Now we already set the cursor position to another point.
We want to keep the cursor position at the same point relative to screen.
Do that only when we are reaching the end of screen so we don't flicker.
*/
var that = this;
var isCursorVisible = app.isPointVisibleInTheDisplayedArea(app.file.textCursor.rectangle.toArray());
if (!isCursorVisible) {
setTimeout(function () {
var y = app.file.textCursor.rectangle.pY1 - that._cursorPreviousPositionCorePixels.pY1;
if (y) {
that._painter._sectionContainer.getSectionWithName(L.CSections.Scroll.name).scrollVerticalWithOffset(y);
}
}, 0);
}
}
this._updateCursorAndOverlay();
this.eachView(this._viewCursors, function (item) {
var viewCursorMarker = item.marker;
if (viewCursorMarker) {
viewCursorMarker.setOpacity(this.isCursorVisible() && this._cursorMarker.getPosition().equals(viewCursorMarker.getPosition()) ? 0 : 1);
}
}, this, true);
},
activateCursor: function () {
this._replayPrintTwipsMsg('invalidatecursor');
},
_isAnyInputFocused: function() {
var hasTunneledDialogOpened = this._map.dialog ? this._map.dialog.hasOpenedDialog() : false;
var hasJSDialogOpened = this._map.jsdialog ? this._map.jsdialog.hasDialogOpened() : false;
var hasJSDialogFocused = L.DomUtil.hasClass(document.activeElement, 'jsdialog');
var commentHasFocus = app.view.commentHasFocus;
var inputHasFocus = $('input:focus').length > 0 || $('textarea.jsdialog:focus').length > 0;
return hasTunneledDialogOpened || hasJSDialogOpened || hasJSDialogFocused
|| commentHasFocus || inputHasFocus;
},
// enable or disable blinking cursor and the cursor overlay depending on
// the state of the document (if the falgs are set)
_updateCursorAndOverlay: function (/*update*/) {
if (app.file.textCursor.visible // only when LOK has told us it is ok
&& this._map.editorHasFocus() // not when document is not focused
&& !this._map.isSearching() // not when searching within the doc
&& !this._isZooming // not when zooming
) {
if (this._innerTextRectMarker)
this._map.addLayer(this._innerTextRectMarker);
this._updateCursorPos();
var scrollSection = app.sectionContainer.getSectionWithName(L.CSections.Scroll.name);
if (!scrollSection.sectionProperties.mouseIsOnVerticalScrollBar && !scrollSection.sectionProperties.mouseIsOnHorizontalScrollBar) {
this._map._textInput.showCursor();
}
var hasMobileWizardOpened = this._map.uiManager.mobileWizard ? this._map.uiManager.mobileWizard.isOpen() : false;
var hasIframeModalOpened = $('.iframe-dialog-modal').is(':visible');
// Don't show the keyboard when the Wizard is visible.
if (!window.mobileWizard && !window.pageMobileWizard &&
!window.insertionMobileWizard && !hasMobileWizardOpened &&
!this._isAnyInputFocused() && !hasIframeModalOpened) {
// If the user is editing, show the keyboard, but don't change
// anything if nothing is changed.
// We will focus map if no comment is being edited (writer only for now).
if (this._docType === 'text') {
var section = app.sectionContainer.getSectionWithName(L.CSections.CommentList.name);
if (!section || !section.sectionProperties.selectedComment || !section.sectionProperties.selectedComment.isEdit())
this._map.focus(true);
}
else
this._map.focus(true);
}
} else {
this._map._textInput.hideCursor();
// Maintain input if a dialog or search-box has the focus.
if (this._map.editorHasFocus() && !this._map.uiManager.isAnyDialogOpen() && !this._map.isSearching()
&& !this._isAnyInputFocused())
this._map.focus(false);
if (this._innerTextRectMarker)
this._map.removeLayer(this._innerTextRectMarker);
}
// when first time we updated the cursor - document is loaded
// let's move cursor to the target
if (this._map.options.docTarget !== '') {
this.goToTarget(this._map.options.docTarget);
this._map.options.docTarget = '';
}
},
// Update colored non-blinking view cursor
_onUpdateViewCursor: function (viewId) {
if (typeof this._viewCursors[viewId] !== 'object' ||
typeof this._viewCursors[viewId].bounds !== 'object') {
return;
}
var pixBounds = this._viewCursors[viewId].corepxBounds;
var viewCursorPos = pixBounds.getTopLeft();
var viewCursorMarker = this._viewCursors[viewId].marker;
var viewCursorVisible = this._viewCursors[viewId].visible;
var viewPart = this._viewCursors[viewId].part;
var viewMode = this._viewCursors[viewId].mode ? this._viewCursors[viewId].mode : 0;
if (!this._map.isViewReadOnly(viewId) &&
viewCursorVisible &&
!this._isZooming &&
!this._isEmptyRectangle(this._viewCursors[viewId].bounds) &&
(this.isWriter() || (this._selectedPart === viewPart && this._selectedMode === viewMode))) {
if (!viewCursorMarker) {
var viewCursorOptions = {
color: L.LOUtil.rgbToHex(this._map.getViewColor(viewId)),
blink: false,
header: true, // we want a 'hat' to our view cursors (which will contain view user names)
headerTimeout: 3000, // hide after some interval
zIndex: viewId,
headerName: this._map.getViewName(viewId)
};
viewCursorMarker = new Cursor(viewCursorPos, pixBounds.getSize(), this._map, viewCursorOptions);
this._viewCursors[viewId].marker = viewCursorMarker;
}
else {
viewCursorMarker.setPositionSize(viewCursorPos, pixBounds.getSize());
}
viewCursorMarker.setOpacity(this.isCursorVisible() && this._cursorMarker.getPosition().equals(viewCursorMarker.getPosition()) ? 0 : 1);
if (!viewCursorMarker.isDomAttached())
viewCursorMarker.add();
}
else if (viewCursorMarker && viewCursorMarker.isDomAttached()) {
viewCursorMarker.remove();
}
if (this._viewCursors[viewId].marker && this._viewCursors[viewId].marker.isDomAttached())
this._viewCursors[viewId].marker.showCursorHeader();
},
updateAllViewCursors: function() {
this.eachView(this._viewCursors, this._onUpdateViewCursor, this, false);
},
updateAllTextViewSelection: function() {
this.eachView(this._viewSelections, this._onUpdateTextViewSelection, this, false);
},
updateAllGraphicViewSelections: function () {
this.eachView(this._graphicViewMarkers, this._onUpdateGraphicViewSelection, this, false);
},
isCursorVisible: function() {
return this._cursorMarker ? this._cursorMarker.isDomAttached() : false;
},
goToViewCursor: function(viewId) {
if (viewId === this._viewId) {
this._onUpdateCursor();
return;
}
if (this._viewCursors[viewId] && this._viewCursors[viewId].visible && !this._isEmptyRectangle(this._viewCursors[viewId].bounds)) {
if (!this._map.getBounds().contains(this._viewCursors[viewId].bounds)) {
var viewCursorPos = this._viewCursors[viewId].bounds.getNorthWest();
this.scrollToPos(viewCursorPos);
}
this._viewCursors[viewId].marker.showCursorHeader();
}
},
_onUpdateTextViewSelection: function (viewId) {
viewId = parseInt(viewId);
var viewPointSet = this._viewSelections[viewId].pointSet;
var viewSelection = this._viewSelections[viewId].selection;
var viewPart = this._viewSelections[viewId].part;
var viewMode = this._viewSelections[viewId].mode ? this._viewSelections[viewId].mode : 0;
if (viewPointSet &&
(this.isWriter() || (this._selectedPart === viewPart && this._selectedMode === viewMode))) {
if (viewSelection) {
if (!this._map.hasInfoForView(viewId)) {
viewSelection.clear();
return;
}
// change previous selections
viewSelection.setPointSet(viewPointSet);
} else {
viewSelection = new CSelections(viewPointSet, this._canvasOverlay,
this._selectionsDataDiv, this._map, true /* isView */, viewId, true /* isText */);
this._viewSelections[viewId].selection = viewSelection;
}
}
else if (viewSelection) {
viewSelection.clear();
}
},
_onUpdateGraphicViewSelection: function (viewId) {
var viewBounds = this._graphicViewMarkers[viewId].bounds;
var viewMarker = this._graphicViewMarkers[viewId].marker;
var viewPart = this._graphicViewMarkers[viewId].part;
var viewMode = this._graphicViewMarkers[viewId].mode;
if (!this._isEmptyRectangle(viewBounds) &&
(this.isWriter() || (this._selectedPart === viewPart && this._selectedMode === viewMode))) {
if (!viewMarker) {
var color = L.LOUtil.rgbToHex(this._map.getViewColor(viewId));
viewMarker = L.rectangle(viewBounds, {
pointerEvents: 'auto',
fill: false,
color: color
});
// Disable autoPan, so the graphic view selection doesn't make the view jump to the popup.
viewMarker.bindPopup(this._map.getViewName(viewId), {autoClose: false, autoPan: false, backgroundColor: color, color: 'white', closeButton: false});
this._graphicViewMarkers[viewId].marker = viewMarker;
}
else {
viewMarker.setBounds(viewBounds);
}
this._viewLayerGroup.addLayer(viewMarker);
}
else if (viewMarker) {
this._viewLayerGroup.removeLayer(viewMarker);
}
},
eachView: function (views, method, context, item) {
for (var key in views) {
method.call(context, item ? views[key] : key);
}
},
// Update dragged graphics selection
_onGraphicMove: function (e) {
if (!e.pos) { return; }
var aPos = this._latLngToTwips(e.pos);
var calcRTL = this.isCalcRTL();
if (e.type === 'graphicmovestart') {
this._graphicMarker.isDragged = true;
this._graphicMarker.setVisible(true);
this._graphicMarker._startPos = aPos;
}
else if (e.type === 'graphicmoveend' && this._graphicMarker._startPos) {
var deltaPos = aPos.subtract(this._graphicMarker._startPos);
if (deltaPos.x === 0 && deltaPos.y === 0) {
this._graphicMarker.isDragged = false;
this._graphicMarker.setVisible(false);
return;
}
var param;
var dragConstraint = this._graphicSelection.extraInfo.dragInfo;
if (dragConstraint) {
if (dragConstraint.dragMethod === 'PieSegmentDragging') {
deltaPos = this._twipsToPixels(deltaPos);
var dx = deltaPos.x;
var dy = deltaPos.y;
var initialOffset = dragConstraint.initialOffset;
var dragDirection = dragConstraint.dragDirection;
var additionalOffset = (dx * dragDirection.x + dy * dragDirection.y) / dragConstraint.range2;
if (additionalOffset < -initialOffset)
additionalOffset = -initialOffset;
else if (additionalOffset > (1.0 - initialOffset))
additionalOffset = 1.0 - initialOffset;
var offset = Math.round((initialOffset + additionalOffset) * 100);
// hijacking the uno:TransformDialog msg for sending the new offset value
// for the pie segment dragging method;
// indeed there isn't any uno msg dispatching on the core side, but a chart controller dispatching
param = {
Action: {
type: 'string',
value: 'PieSegmentDragging'
},
Offset: {
type: 'long',
value: offset
}
};
}
}
else {
var newPos = new L.Point(
// Choose the logical left of the shape.
this._graphicSelectionTwips.min.x + deltaPos.x,
this._graphicSelectionTwips.min.y + deltaPos.y);
var size = this._graphicSelectionTwips.getSize();
if (calcRTL) {
// make x coordinate of newPos +ve
newPos.x = -newPos.x;
}
// try to keep shape inside document
if (newPos.x + size.x > this._docWidthTwips)
newPos.x = this._docWidthTwips - size.x;
if (newPos.x < 0)
newPos.x = 0;
if (newPos.y + size.y > this._docHeightTwips)
newPos.y = this._docHeightTwips - size.y;
if (newPos.y < 0)
newPos.y = 0;
if (this.isCalc() && this.options.printTwipsMsgsEnabled) {
newPos = this.sheetGeometry.getPrintTwipsPointFromTile(newPos);
}
// restore the sign(negative) of x coordinate.
if (calcRTL) {
newPos.x = -newPos.x;
}
param = {
TransformPosX: {
type: 'long',
value: newPos.x
},
TransformPosY: {
type: 'long',
value: newPos.y
}
};
}
this._map.sendUnoCommand('.uno:TransformDialog ', param);
this._graphicMarker.isDragged = false;
this._graphicMarker.setVisible(false);
}
},
// Update dragged graphics selection resize.
_onGraphicEdit: function (e) {
if (!e.pos) { return; }
if (!e.handleId) { return; }
var calcRTL = this.isCalcRTL();
var aPos = this._latLngToTwips(e.pos);
var selMin = this._graphicSelectionTwips.min;
var selMax = this._graphicSelectionTwips.max;
var handleId = e.handleId;
if (e.type === 'scalestart') {
this._graphicMarker.isDragged = true;
this._graphicMarker.setVisible(true);
if (selMax.x - selMin.x < 2)
this._graphicMarker.dragHorizDir = 0; // overlapping handles
else if (Math.abs(selMin.x - aPos.x) < 2)
this._graphicMarker.dragHorizDir = -1; // left handle
else if (Math.abs(selMax.x - aPos.x) < 2)
this._graphicMarker.dragHorizDir = 1; // right handle
if (selMax.y - selMin.y < 2)
this._graphicMarker.dragVertDir = 0; // overlapping handles
else if (Math.abs(selMin.y - aPos.y) < 2)
this._graphicMarker.dragVertDir = -1; // up handle
else if (Math.abs(selMax.y - aPos.y) < 2)
this._graphicMarker.dragVertDir = 1; // down handle
}
else if (e.type === 'scaleend') {
// fill params for uno command
var param = {
HandleNum: {
type: 'long',
value: handleId
},
NewPosX: {
type: 'long',
// In Calc RTL mode ensure that we send positive X coordinates.
value: calcRTL ? -aPos.x : aPos.x
},
NewPosY: {
type: 'long',
value: aPos.y
}
};
if (e.ordNum)
{
var glueParams = {
OrdNum: {
type: 'long',
value: e.ordNum
}
};
param = L.Util.extend({}, param, glueParams);
}
this._map.sendUnoCommand('.uno:MoveShapeHandle', param);
this._graphicMarker.isDragged = false;
this._graphicMarker.setVisible(false);
this._graphicMarker.dragHorizDir = undefined;
this._graphicMarker.dragVertDir = undefined;
}
},
_onGraphicRotate: function (e) {
if (e.type === 'rotatestart') {
this._graphicMarker.isDragged = true;
this._graphicMarker.setVisible(true);
}
else if (e.type === 'rotateend') {
var center = this._graphicSelectionTwips.getCenter();
if (this.isCalc() && this.options.printTwipsMsgsEnabled) {
center = this.sheetGeometry.getPrintTwipsPointFromTile(center);
}
var param = {
TransformRotationDeltaAngle: {
type: 'long',
value: (((e.rotation * 18000) / Math.PI))
},
TransformRotationX: {
type: 'long',
value: center.x
},
TransformRotationY: {
type: 'long',
value: center.y
}
};
this._map.sendUnoCommand('.uno:TransformDialog ', param);
this._graphicMarker.isDragged = false;
this._graphicMarker.setVisible(false);
}
},
// Update dragged text selection.
_onSelectionHandleDrag: function (e) {
if (e.type === 'drag') {
window.IgnorePanning = true;
e.target.isDragged = true;
if (!e.originalEvent.pageX && !e.originalEvent.pageY) {
return;
}
// This is rather hacky, but it seems to be the only way to make the
// marker follow the mouse cursor if the document is autoscrolled under
// us. (This can happen when we're changing the selection if the cursor
// moves somewhere that is considered off screen.)
// Onscreen position of the cursor, i.e. relative to the browser window
var boundingrect = e.target._icon.getBoundingClientRect();
var cursorPos = L.point(boundingrect.left, boundingrect.top);
var expectedPos = L.point(e.originalEvent.pageX, e.originalEvent.pageY).subtract(e.target.dragging._draggable.startOffset);
// Dragging the selection handles vertically more than one line on a touch
// device is more or less impossible without this hack.
if (!(typeof e.originalEvent.type === 'string' && e.originalEvent.type === 'touchmove')) {
// If the map has been scrolled, but the cursor hasn't been updated yet, then
// the current mouse position differs.
if (!expectedPos.equals(cursorPos)) {
var correction = expectedPos.subtract(cursorPos);
e.target.dragging._draggable._startPoint = e.target.dragging._draggable._startPoint.add(correction);
e.target.dragging._draggable._startPos = e.target.dragging._draggable._startPos.add(correction);
e.target.dragging._draggable._newPos = e.target.dragging._draggable._newPos.add(correction);
e.target.dragging._draggable._updatePosition();
}
}
var containerPos = new L.Point(expectedPos.x - this._map._container.getBoundingClientRect().left,
expectedPos.y - this._map._container.getBoundingClientRect().top);
containerPos = containerPos.add(e.target.dragging._draggable.startOffset);
this._map.fire('handleautoscroll', {pos: containerPos, map: this._map});
}
if (e.type === 'dragend') {
window.IgnorePanning = undefined;
e.target.isDragged = false;
this._map.fire('scrollvelocity', {vx: 0, vy: 0});
}
var aPos = this._latLngToTwips(e.target.getLatLng());
if (this._selectionHandles.start === e.target) {
this._postSelectTextEvent('start', aPos.x, aPos.y);
}
else if (this._selectionHandles.end === e.target) {
this._postSelectTextEvent('end', aPos.x, aPos.y);
}
},
// Update dragged text selection.
_onCellResizeMarkerDrag: function (e) {
if (e.type === 'dragstart') {
e.target.isDragged = true;
}
else if (e.type === 'drag') {
var event = e.originalEvent;
if (e.originalEvent.touches && e.originalEvent.touches.length > 0) {
event = e.originalEvent.touches[0];
}
if (!event.pageX && !event.pageY) {
return;
}
// handle scrolling
// This is rather hacky, but it seems to be the only way to make the
// marker follow the mouse cursor if the document is autoscrolled under
// us. (This can happen when we're changing the selection if the cursor
// moves somewhere that is considered off screen.)
// Onscreen position of the cursor, i.e. relative to the browser window
var boundingrect = e.target._icon.getBoundingClientRect();
var cursorPos = L.point(boundingrect.left, boundingrect.top);
var expectedPos = L.point(event.pageX, event.pageY).subtract(e.target.dragging._draggable.startOffset);
// Dragging the selection handles vertically more than one line on a touch
// device is more or less impossible without this hack.
if (!(typeof e.originalEvent.type === 'string' && e.originalEvent.type === 'touchmove')) {
// If the map has been scrolled, but the cursor hasn't been updated yet, then
// the current mouse position differs.
if (!expectedPos.equals(cursorPos)) {
var correction = expectedPos.subtract(cursorPos);
e.target.dragging._draggable._startPoint = e.target.dragging._draggable._startPoint.add(correction);
e.target.dragging._draggable._startPos = e.target.dragging._draggable._startPos.add(correction);
e.target.dragging._draggable._newPos = e.target.dragging._draggable._newPos.add(correction);
e.target.dragging._draggable._updatePosition();
}
}
var containerPos = new L.Point(expectedPos.x - this._map._container.getBoundingClientRect().left,
expectedPos.y - this._map._container.getBoundingClientRect().top);
containerPos = containerPos.add(e.target.dragging._draggable.startOffset);
this._map.fire('handleautoscroll', {pos: containerPos, map: this._map});
}
else if (e.type === 'dragend') {
e.target.isDragged = false;
// handle scrolling
this._map.focus();
this._map.fire('scrollvelocity', {vx: 0, vy: 0});
}
// modify the mouse position - move to center of the marker
var aMousePosition = e.target.getLatLng();
aMousePosition = this._map.project(aMousePosition);
var size;
if (this._cellResizeMarkerStart === e.target) {
size = this._cellResizeMarkerStart._icon.getBoundingClientRect();
}
else if (this._cellResizeMarkerEnd === e.target) {
size = this._cellResizeMarkerEnd._icon.getBoundingClientRect();
}
aMousePosition = aMousePosition.add(new L.Point(size.width / 2, size.height / 2));
aMousePosition = this._map.unproject(aMousePosition);
aMousePosition = this._latLngToTwips(aMousePosition);
if (this._cellResizeMarkerStart === e.target) {
this._postSelectTextEvent('start', aMousePosition.x, aMousePosition.y);
if (e.type === 'dragend') {
this._onUpdateCellResizeMarkers();
window.IgnorePanning = undefined;
}
}
else if (this._cellResizeMarkerEnd === e.target) {
this._postSelectTextEvent('end', aMousePosition.x, aMousePosition.y);
if (e.type === 'dragend') {
this._onUpdateCellResizeMarkers();
window.IgnorePanning = undefined;
}
}
},
_onReferenceMarkerDrag: function(e) {
if (e.type === 'dragstart') {
e.target.isDragged = true;
window.IgnorePanning = true;
}
else if (e.type === 'drag') {
var startPos = this._map.project(this._referenceMarkerStart.getLatLng());
var startSize = this._referenceMarkerStart._icon.getBoundingClientRect();
startPos = startPos.add(new L.Point(startSize.width, startSize.height));
var start = this.sheetGeometry.getCellFromPos(this._latLngToTwips(this._map.unproject(startPos)), 'tiletwips');
var endPos = this._map.project(this._referenceMarkerEnd.getLatLng());
var endSize = this._referenceMarkerEnd._icon.getBoundingClientRect();
endPos = endPos.subtract(new L.Point(endSize.width / 2, endSize.height / 2));
var end = this.sheetGeometry.getCellFromPos(this._latLngToTwips(this._map.unproject(endPos)), 'tiletwips');
this._sendReferenceRangeCommand(start.x, start.y, end.x, end.y);
}
else if (e.type === 'dragend') {
e.target.isDragged = false;
window.IgnorePanning = undefined;
this._updateReferenceMarks();
}
},
_sendReferenceRangeCommand: function(startCol, startRow, endCol, endRow) {
this._map.sendUnoCommand(
'.uno:CurrentFormulaRange?StartCol=' + startCol +
'&StartRow=' + startRow +
'&EndCol=' + endCol +
'&EndRow=' + endRow +
'&Table=' + this._map._docLayer._selectedPart
);
},
_onUpdateGraphicInnerTextArea: function (rect, force) {
var topLeftTwips = new L.Point(rect[0], rect[1]);
var offset = new L.Point(rect[2], rect[3]);
var bottomRightTwips = topLeftTwips.add(offset);
this._innerTextRectTwips = this._getGraphicSelectionRectangle(
new L.Bounds(topLeftTwips, bottomRightTwips));
this._innerTextRect = new L.LatLngBounds(
this._twipsToLatLng(this._innerTextRectTwips.getTopLeft(), this._map.getZoom()),
this._twipsToLatLng(this._innerTextRectTwips.getBottomRight(), this._map.getZoom()));
if (this._innerTextRectMarker)
this._map.removeLayer(this._innerTextRectMarker);
this._innerTextRectMarker = L.svgGroup(this._innerTextRect, {
draggable: true,
dragConstraint: undefined,
transform: false,
stroke: false,
fillOpacity: 0,
fill: true,
isText: true
});
if (force)
this._map.addLayer(this._innerTextRectMarker);
},
// Update group layer selection handler.
_onUpdateGraphicSelection: function () {
if (this._graphicSelection && !this._isEmptyRectangle(this._graphicSelection)) {
// Hide the keyboard on graphic selection, unless cursor is visible.
// Don't interrupt editing in dialogs
if (!this._isAnyInputFocused())
this._map.focus(this.isCursorVisible());
if (this._graphicMarker) {
this._graphicMarker.removeEventParent(this._map);
this._graphicMarker.off('scalestart scaleend', this._onGraphicEdit, this);
this._graphicMarker.off('rotatestart rotateend', this._onGraphicRotate, this);
if (this._graphicMarker.dragging)
this._graphicMarker.dragging.disable();
this._graphicMarker.transform.disable();
this._map.removeLayer(this._graphicMarker);
}
if (!this._map.isEditMode()) {
return;
}
var extraInfo = this._graphicSelection.extraInfo;
this._graphicMarker = L.svgGroup(this._graphicSelection, {
draggable: extraInfo.isDraggable,
dragConstraint: extraInfo.dragInfo,
svg: this._map._cacheSVG[extraInfo.id],
transform: true,
stroke: false,
fillOpacity: 0,
fill: true
});
if (!this._graphicMarker) {
this._map.fire('error', {msg: 'Graphic marker initialization', cmd: 'marker', kind: 'failed', id: 1});
return;
}
if (extraInfo.innerTextRect) {
this._onUpdateGraphicInnerTextArea(extraInfo.innerTextRect);
}
this._graphicMarker.on('graphicmovestart graphicmoveend', this._onGraphicMove, this);
this._graphicMarker.on('scalestart scaleend', this._onGraphicEdit, this);
this._graphicMarker.on('rotatestart rotateend', this._onGraphicRotate, this);
this._map.addLayer(this._graphicMarker);
if (extraInfo.isDraggable)
this._graphicMarker.dragging.enable();
this._graphicMarker.transform.enable({
scaling: extraInfo.isResizable,
rotation: extraInfo.isRotatable && !this.hasTableSelection(),
uniformScaling: this._shouldScaleUniform(extraInfo),
isRotated: !this._isGraphicAngleDivisibleBy90(),
handles: (extraInfo.handles) ? extraInfo.handles.kinds || [] : [],
shapes: (extraInfo.GluePoints) ? extraInfo.GluePoints.shapes : [],
shapeType: extraInfo.type,
scaleSouthAndEastOnly: this.hasTableSelection()});
if (extraInfo.dragInfo && extraInfo.dragInfo.svg) {
this._graphicMarker.removeEmbeddedSVG();
this._graphicMarker.addEmbeddedSVG(extraInfo.dragInfo.svg);
}
this._hasActiveSelection = true;
}
else if (this._graphicMarker) {
this._graphicMarker.off('graphicmovestart graphicmoveend', this._onGraphicMove, this);
this._graphicMarker.off('scalestart scaleend', this._onGraphicEdit, this);
this._graphicMarker.off('rotatestart rotateend', this._onGraphicRotate, this);
if (this._graphicMarker.dragging)
this._graphicMarker.dragging.disable();
this._graphicMarker.transform.disable();
this._map.removeLayer(this._graphicMarker);
this._graphicMarker.isDragged = false;
this._graphicMarker.setVisible(false);
}
this._updateCursorAndOverlay();
},
// TODO: used only in calc: move to CalcTileLayer
_onUpdateCellCursor: function (onPgUpDn, scrollToCursor, sameAddress) {
this._onUpdateCellResizeMarkers();
if (app.calc.cellCursorVisible) {
var mapBounds = this._map.getBounds();
if (scrollToCursor && (!this._prevCellCursorAddress || !app.calc.cellAddress.equals(this._prevCellCursorAddress.toArray())) &&
!this._map.calcInputBarHasFocus()) {
var scroll = this._calculateScrollForNewCellCursor();
window.app.console.assert(scroll instanceof L.LatLng, '_calculateScrollForNewCellCursor returned wrong type');
if (scroll.lng !== 0 || scroll.lat !== 0) {
var newCenter = mapBounds.getCenter();
newCenter.lng += scroll.lng;
newCenter.lat += scroll.lat;
this.scrollToPos(newCenter);
}
this._prevCellCursorAddress = app.calc.cellAddress.clone();
}
if (onPgUpDn) {
this._cellCursorOnPgUp = null;
this._cellCursorOnPgDn = null;
}
var corePxBounds = new L.Bounds(new L.Point(app.calc.cellCursorRectangle.pX1, app.calc.cellCursorRectangle.pY1),
new L.Point(app.calc.cellCursorRectangle.pX2, app.calc.cellCursorRectangle.pY2));
if (this._cellCursorMarker) {
this._cellCursorMarker.setBounds(corePxBounds);
this._removeCellDropDownArrow();
}
else {
var cursorStyle = new CStyleData(this._cursorDataDiv);
var weight = cursorStyle.getFloatPropWithoutUnit('border-top-width');
this._cellCursorMarker = new CCellCursor(
corePxBounds,
{
name: 'cell-cursor',
pointerEvents: 'none',
fill: false,
color: cursorStyle.getPropValue('border-top-color'),
weight: Math.round(weight * app.dpiScale)
});
if (!this._cellCursorMarker) {
this._map.fire('error', {msg: 'Cell Cursor marker initialization', cmd: 'cellCursor', kind: 'failed', id: 1});
return;
}
this._canvasOverlay.initPathGroup(this._cellCursorMarker);
}
this._addCellDropDownArrow();
var focusOutOfDocument = document.activeElement === document.body;
var dontFocusDocument = this._isAnyInputFocused() || focusOutOfDocument;
var dontStealFocus = sameAddress && this._map.calcInputBarHasFocus();
dontFocusDocument = dontFocusDocument || dontStealFocus;
// when the cell cursor is moving, the user is in the document,
// and the focus should leave the cell input bar
// exception: when dialog opened don't focus the document
if (!dontFocusDocument)
this._map.fire('editorgotfocus');
}
else if (this._cellCursorMarker) {
this._canvasOverlay.removePathGroup(this._cellCursorMarker);
this._cellCursorMarker = undefined;
}
this._removeCellDropDownArrow();
this._closeURLPopUp();
},
_onValidityListButtonMsg: function(textMsg) {
var strXY = textMsg.match(/\d+/g);
var validatedCellAddress = new app.definitions.simplePoint(parseInt(strXY[0]), parseInt(strXY[1])); // Cell address of the validility list.
var show = parseInt(strXY[2]) === 1;
if (show) {
if (this._validatedCellAddress && !validatedCellAddress.equals(this._validatedCellAddress.toArray())) {
this._validatedCellAddress = null;
this._removeCellDropDownArrow();
}
this._validatedCellAddress = validatedCellAddress;
this._addCellDropDownArrow();
}
else if (this._validatedCellAddress && validatedCellAddress.equals(this._validatedCellAddress.toArray())) {
this._validatedCellAddress = null;
this._removeCellDropDownArrow();
}
},
_onValidityInputHelpMsg: function(textMsg) {
var message = textMsg.replace('validityinputhelp: ', '');
message = JSON.parse(message);
var icon = L.divIcon({
html: '<div class="input-help"><h4 id="input-help-title"></h4><p id="input-help-content"></p></div>',
iconSize: [0, 0],
iconAnchor: [0, 0]
});
this._removeInputHelpMarker();
var pos = this._twipsToLatLng({ x: app.calc.cellCursorRectangle.x2, y: app.calc.cellCursorRectangle.y1 });
var inputHelpMarker = L.marker(pos, { icon: icon });
inputHelpMarker.addTo(this._map);
document.getElementById('input-help-title').innerText = message.title;
document.getElementById('input-help-content').innerText = message.content;
this._inputHelpPopUp = inputHelpMarker;
},
_addCellDropDownArrow: function () {
if (this._validatedCellAddress && app.calc.cellCursorVisible && this._validatedCellAddress.equals(app.calc.cellAddress.toArray())) {
if (!app.sectionContainer.getSectionWithName('DropDownArrow')) {
let position = new app.definitions.simplePoint(app.calc.cellCursorRectangle.x2, app.calc.cellCursorRectangle.y1);
let dropDownSection = new app.definitions.htmlObjectSection('DropDownArrow', 16, 16, position, 'spreadsheet-drop-down-marker'); // spreadsheet-drop-down-marker
app.sectionContainer.addSection(dropDownSection);
dropDownSection.onClick = function() {
dropDownSection.stopPropagating(); // This will be enough after we remove leaflet.
if (this._validatedCellAddress && app.calc.cellCursorVisible && this._validatedCellAddress.equals(app.calc.cellAddress.toArray())) {
this._map.sendUnoCommand('.uno:DataSelect');
}
}.bind(this);
dropDownSection.getHTMLObject().onclick = function(e) {
e.stopPropagation(); // We need this because leaflet can catch the event.
dropDownSection.onClick();
};
}
else {
app.sectionContainer.getSectionWithName('DropDownArrow').setPosition(app.calc.cellCursorRectangle.pX2, app.calc.cellCursorRectangle.pY1);
}
}
},
_removeCellDropDownArrow: function () {
if (!this._validatedCellAddress)
app.sectionContainer.removeSection('DropDownArrow');
},
_onUpdateCellResizeMarkers: function () {
var selectionOnDesktop = window.mode.isDesktop() && (this._cellSelectionArea || app.calc.cellCursorVisible);
if (!selectionOnDesktop &&
(!this._cellCSelections.empty() || app.calc.cellCursorVisible)) {
if (this._isEmptyRectangle(this._cellSelectionArea) && !app.calc.cellCursorVisible) {
return;
}
let latLngCursor = this._simpleRectangleToLatLngBounds(app.calc.cellCursorRectangle.clone());
var cellRectangle = this._cellSelectionArea ? this._cellSelectionArea : latLngCursor;
if (!this._cellResizeMarkerStart.isDragged) {
this._map.addLayer(this._cellResizeMarkerStart);
var posStart = this._map.project(cellRectangle.getNorthWest());
var sizeStart = this._cellResizeMarkerStart._icon.getBoundingClientRect();
posStart = posStart.subtract(new L.Point(sizeStart.width / 2, sizeStart.height / 2));
posStart = this._map.unproject(posStart);
this._cellResizeMarkerStart.setLatLng(posStart);
}
if (!this._cellResizeMarkerEnd.isDragged) {
this._map.addLayer(this._cellResizeMarkerEnd);
var posEnd = this._map.project(cellRectangle.getSouthEast());
var sizeEnd = this._cellResizeMarkerEnd._icon.getBoundingClientRect();
posEnd = posEnd.subtract(new L.Point(sizeEnd.width / 2, sizeEnd.height / 2));
posEnd = this._map.unproject(posEnd);
this._cellResizeMarkerEnd.setLatLng(posEnd);
}
}
else if (selectionOnDesktop) {
this._map.removeLayer(this._cellResizeMarkerStart);
this._map.removeLayer(this._cellResizeMarkerEnd);
} else {
this._map.removeLayer(this._cellResizeMarkerStart);
this._map.removeLayer(this._cellResizeMarkerEnd);
}
},
// Update text selection handlers.
_onUpdateTextSelection: function () {
this._onUpdateCellResizeMarkers();
var startMarker = this._selectionHandles['start'];
var endMarker = this._selectionHandles['end'];
if (this._map.editorHasFocus() && (!this._textCSelections.empty() || startMarker.isDragged || endMarker.isDragged)) {
this._updateMarkers();
}
else {
this._updateMarkers();
this._removeSelection();
}
},
_removeSelection: function() {
this._textSelectionStart = null;
this._textSelectionEnd = null;
this._selectedTextContent = '';
for (var key in this._selectionHandles) {
this._map.removeLayer(this._selectionHandles[key]);
this._selectionHandles[key].isDragged = false;
}
this._textCSelections.clear();
},
_updateMarkers: function() {
if (!app.file.textCursor.visible)
return;
var startMarker = this._selectionHandles['start'];
var endMarker = this._selectionHandles['end'];
if (!startMarker || !endMarker ||
this._isEmptyRectangle(this._textSelectionStart) ||
this._isEmptyRectangle(this._textSelectionEnd)) {
return;
}
var startPos = this._map.project(this._textSelectionStart.getSouthWest());
var endPos = this._map.project(this._textSelectionEnd.getSouthWest());
var startMarkerPos = this._map.project(startMarker.getLatLng());
// CalcRTL: position from core are in document coordinates. Conversion to layer coordinates for each maker is done
// in L.Layer.getLayerPositionVisibility(). Icons of RTL "start" and "end" has to be interchanged.
var calcRTL = this.isCalcRTL();
if (startMarkerPos.distanceTo(endPos) < startMarkerPos.distanceTo(startPos) && startMarker._icon && endMarker._icon) {
// if the start marker is actually closer to the end of the selection
// reverse icons and markers
L.DomUtil.removeClass(startMarker._icon, calcRTL ? 'leaflet-selection-marker-end' : 'leaflet-selection-marker-start');
L.DomUtil.removeClass(endMarker._icon, calcRTL ? 'leaflet-selection-marker-start' : 'leaflet-selection-marker-end');
L.DomUtil.addClass(startMarker._icon, calcRTL ? 'leaflet-selection-marker-start' : 'leaflet-selection-marker-end');
L.DomUtil.addClass(endMarker._icon, calcRTL ? 'leaflet-selection-marker-end' : 'leaflet-selection-marker-start');
var tmp = startMarker;
startMarker = endMarker;
endMarker = tmp;
}
else if (startMarker._icon && endMarker._icon) {
// normal markers and normal icons
L.DomUtil.removeClass(startMarker._icon, calcRTL ? 'leaflet-selection-marker-start' : 'leaflet-selection-marker-end');
L.DomUtil.removeClass(endMarker._icon, calcRTL ? 'leaflet-selection-marker-end' : 'leaflet-selection-marker-start');
L.DomUtil.addClass(startMarker._icon, calcRTL ? 'leaflet-selection-marker-end' : 'leaflet-selection-marker-start');
L.DomUtil.addClass(endMarker._icon, calcRTL ? 'leaflet-selection-marker-start' : 'leaflet-selection-marker-end');
}
if (!startMarker.isDragged) {
var pos = this._map.project(this._textSelectionStart.getSouthWest());
pos = this._map.unproject(pos);
startMarker.setLatLng(pos);
this._map.addLayer(startMarker);
}
if (!endMarker.isDragged) {
pos = this._map.project(this._textSelectionEnd.getSouthEast());
pos = this._map.unproject(pos);
endMarker.setLatLng(pos);
this._map.addLayer(endMarker);
}
},
hasGraphicSelection: function() {
return (this._graphicSelection !== null &&
!this._isEmptyRectangle(this._graphicSelection));
},
_onDragOver: function (e) {
e = e.originalEvent;
e.preventDefault();
},
_onDrop: function (e) {
// Move the cursor, so that the insert position is as close to the drop coordinates as possible.
var latlng = e.latlng;
var docLayer = this._map._docLayer;
var mousePos = docLayer._latLngToTwips(latlng);
var count = 1;
var buttons = 1;
var modifier = this._map.keyboard.modifier;
this._postMouseEvent('buttondown', mousePos.x, mousePos.y, count, buttons, modifier);
this._postMouseEvent('buttonup', mousePos.x, mousePos.y, count, buttons, modifier);
e = e.originalEvent;
e.preventDefault();
if (this._map._clip) {
// Always capture the html content separate as we may lose it when we
// pass the clipboard data to a different context (async calls, f.e.).
var htmlText = e.dataTransfer.getData('text/html');
this._map._clip.dataTransferToDocument(e.dataTransfer, /* preferInternal = */ false, htmlText);
}
},
_onDragStart: function () {
this._map.on('moveend', this._updateScrollOffset, this);
},
// This is really just called on zoomend
_fitWidthZoom: function (e, maxZoom) {
if (this.isCalc())
return;
if (isNaN(this._docWidthTwips)) { return; }
var oldSize = e ? e.oldSize : this._map.getSize();
var newSize = e ? e.newSize : this._map.getSize();
newSize.x *= app.dpiScale;
newSize.y *= app.dpiScale;
oldSize.x *= app.dpiScale;
oldSize.y *= app.dpiScale;
if (this.isWriter() && newSize.x - oldSize.x === 0) { return; }
var widthTwips = newSize.x * this._tileWidthTwips / this._tileSize;
var ratio = widthTwips / this._docWidthTwips;
maxZoom = maxZoom ? maxZoom : 10;
var zoom = this._map.getScaleZoom(ratio, 10);
zoom = Math.min(maxZoom, Math.max(0.1, zoom));
// Not clear why we wanted to zoom in the past.
// This resets the view & scroll area and does a 'panTo'
// to keep the cursor in view.
// But of course, zoom to fit the first time.
if (this._firstFitDone)
zoom = this._map._zoom;
this._firstFitDone = true;
if (zoom > 1)
zoom = Math.floor(zoom);
this._map.setZoom(zoom, {animate: false});
},
_onCurrentPageUpdate: function () {
if (!this._map)
return;
var mapCenter = this._map.project(this._map.getCenter());
if (!this._partPageRectanglesPixels || !(this._currentPage >= 0) || this._currentPage >= this._partPageRectanglesPixels.length ||
this._partPageRectanglesPixels[this._currentPage].contains(mapCenter)) {
// page number has not changed
return;
}
for (var i = 0; i < this._partPageRectanglesPixels.length; i++) {
if (this._partPageRectanglesPixels[i].contains(mapCenter)) {
this._currentPage = i;
this._map.fire('pagenumberchanged', {
currentPage: this._currentPage,
pages: this._pages,
docType: this._docType
});
return;
}
}
},
// Cells can change position during changes of zoom level in calc
// hence we need to request an updated cell cursor position for this level.
_onCellCursorShift: function (force) {
if ((this._cellCursorMarker && !this.options.sheetGeometryDataEnabled) || force) {
this.requestCellCursor();
}
},
requestCellCursor: function() {
app.socket.sendMessage('commandvalues command=.uno:CellCursor'
+ '?outputHeight=' + this._tileWidthPx
+ '&outputWidth=' + this._tileHeightPx
+ '&tileHeight=' + this._tileWidthTwips
+ '&tileWidth=' + this._tileHeightTwips);
},
_invalidateAllPreviews: function () {
this._previewInvalidations = [];
for (var key in this._map._docPreviews) {
var preview = this._map._docPreviews[key];
preview.invalid = true;
this._previewInvalidations.push(new L.Bounds(new L.Point(0, 0), new L.Point(preview.maxWidth, preview.maxHeight)));
}
this._invalidatePreviews();
},
_invalidatePreviews: function () {
if (this._map && this._map._docPreviews && this._previewInvalidations.length > 0) {
var toInvalidate = {};
for (var i = 0; i < this._previewInvalidations.length; i++) {
var invalidBounds = this._previewInvalidations[i];
for (var key in this._map._docPreviews) {
// find preview tiles that need to be updated and add them in a set
var preview = this._map._docPreviews[key];
if (preview.index >= 0 && this.isWriter()) {
// we have a preview for a page
if (preview.invalid || (this._partPageRectanglesTwips.length > preview.index &&
invalidBounds.intersects(this._partPageRectanglesTwips[preview.index]))) {
toInvalidate[key] = true;
}
}
else if (preview.index >= 0) {
// we have a preview for a part
if (preview.invalid || preview.index === this._selectedPart ||
(preview.index === this._prevSelectedPart && this._prevSelectedPartNeedsUpdate)) {
// if the current part needs its preview updated OR
// the part has been changed and we need to update the previous part preview
if (preview.index === this._prevSelectedPart) {
this._prevSelectedPartNeedsUpdate = false;
}
toInvalidate[key] = true;
}
}
else {
// we have a custom preview
var bounds = new L.Bounds(
new L.Point(preview.tilePosX, preview.tilePosY),
new L.Point(preview.tilePosX + preview.tileWidth, preview.tilePosY + preview.tileHeight));
if (preview.invalid || (preview.part === this._selectedPart ||
(preview.part === this._prevSelectedPart && this._prevSelectedPartNeedsUpdate)) &&
invalidBounds.intersects(bounds)) {
// if the current part needs its preview updated OR
// the part has been changed and we need to update the previous part preview
if (preview.index === this._prevSelectedPart) {
this._prevSelectedPartNeedsUpdate = false;
}
toInvalidate[key] = true;
}
}
}
}
for (key in toInvalidate) {
// update invalid preview tiles
preview = this._map._docPreviews[key];
if (preview.autoUpdate) {
if (preview.index >= 0) {
this._map.getPreview(preview.id, preview.index, preview.maxWidth, preview.maxHeight, {autoUpdate: true});
}
else {
this._map.getCustomPreview(preview.id, preview.part, preview.width, preview.height, preview.tilePosX,
preview.tilePosY, preview.tileWidth, preview.tileHeight, {autoUpdate: true});
}
}
}
}
this._previewInvalidations = [];
},
_onFormFieldButtonMsg: function (textMsg) {
textMsg = textMsg.substring('formfieldbutton:'.length + 1);
var json = JSON.parse(textMsg);
if (json.action === 'show') {
this._formFieldButton = new L.FormFieldButton(json);
this._map.addLayer(this._formFieldButton);
} else if (this._formFieldButton) {
this._map.removeLayer(this._formFieldButton);
}
},
// converts rectangle in print-twips to tile-twips rectangle of the smallest cell-range that encloses it.
_convertToTileTwipsSheetArea: function (rectangle) {
if (!(rectangle instanceof L.Bounds) || !this.options.printTwipsMsgsEnabled || !this.sheetGeometry) {
return rectangle;
}
return this.sheetGeometry.getTileTwipsSheetAreaFromPrint(rectangle);
},
_getGraphicSelectionRectangle: function (rectangle) {
if (!(rectangle instanceof L.Bounds) || !this.options.printTwipsMsgsEnabled || !this.sheetGeometry) {
return rectangle;
}
// Calc
var rectSize = rectangle.getSize();
var newTopLeft = this.sheetGeometry.getTileTwipsPointFromPrint(rectangle.getTopLeft());
if (this.isLayoutRTL()) { // Convert to negative display-twips coordinates.
newTopLeft.x = -newTopLeft.x;
rectSize.x = -rectSize.x;
}
return new L.Bounds(newTopLeft, newTopLeft.add(rectSize));
},
_convertCalcTileTwips: function (point, offset) {
if (!this.options.printTwipsMsgsEnabled || !this.sheetGeometry)
return point;
var newPoint = new L.Point(parseInt(point.x), parseInt(point.y));
var _offset = offset ? new L.Point(parseInt(offset.x), parseInt(offset.y)) : this._shapeGridOffset;
return newPoint.add(_offset);
},
_getEditCursorRectangle: function (msgObj) {
if (typeof msgObj !== 'object' || !Object.prototype.hasOwnProperty.call(msgObj,'rectangle')) {
window.app.console.error('invalid edit cursor message');
return undefined;
}
return L.Bounds.parse(msgObj.rectangle);
},
_getTextSelectionRectangles: function (textMsg) {
if (typeof textMsg !== 'string') {
window.app.console.error('invalid text selection message');
return [];
}
return L.Bounds.parseArray(textMsg);
},
// Needed for the split-panes feature to determine the active split-pane.
// Needs to be implemented by the app specific TileLayer.
getCursorPos: function () {
window.app.console.error('No implementations available for getCursorPos!');
return new L.Point(0, 0);
},
getPaneLatLngRectangles: function () {
var map = this._map;
if (!this._splitPanesContext) {
return [ map.getBounds() ];
}
// These paneRects are in core pixels.
var paneRects = this._splitPanesContext.getPxBoundList();
window.app.console.assert(paneRects.length, 'number of panes cannot be zero!');
return paneRects.map(function (pxBound) {
return new L.LatLngBounds(
map.unproject(pxBound.getTopLeft().divideBy(app.dpiScale)),
map.unproject(pxBound.getBottomRight().divideBy(app.dpiScale))
);
});
},
/// onlyThread - takes annotation indicating which thread will be generated
getCommentWizardStructure: function(menuStructure, onlyThread) {
var customTitleBar = L.DomUtil.create('div');
L.DomUtil.addClass(customTitleBar, 'mobile-wizard-titlebar-btn-container');
var title = L.DomUtil.create('span', '', customTitleBar);
title.innerText = _('Comment');
var button = L.DomUtil.createWithId('button', 'insert_comment', customTitleBar);
L.DomUtil.addClass(button, 'mobile-wizard-titlebar-btn');
button.innerText = '+';
button.onclick = this._map.insertComment.bind(this._map);
if (menuStructure === undefined) {
menuStructure = {
id : 'comment',
type : 'mainmenu',
enabled : true,
text : _('Comment'),
executionType : 'menu',
data : [],
children : []
};
if (app.isCommentEditingAllowed())
menuStructure['customTitle'] = customTitleBar;
}
app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).createCommentStructure(menuStructure, onlyThread);
if (menuStructure.children.length === 0) {
var noComments = {
id: 'emptyWizard',
enable: true,
type: 'emptyCommentWizard',
text: _('No Comments'),
children: []
};
menuStructure['children'].push(noComments);
}
return menuStructure;
},
_openCommentWizard: function(annotation) {
window.commentWizard = true;
var menuData = this._map._docLayer.getCommentWizardStructure();
this._map.fire('mobilewizard', {data: menuData});
// if annotation is provided we can select perticular comment
if (annotation) {
$('#comment' + annotation.sectionProperties.data.id).click();
}
},
_saveMessageForReplay: function (textMsg, viewId) {
// We will not get some messages (with coordinates)
// from core when zoom changes because print-twips coordinates are zoom-invariant. So we need to
// remember the last version of them and replay, when zoom is changed.
// In calc we need to replay the messages when sheet-geometry changes too. This is because it is possible for
// the updated print-twips messages to arrive before the sheet-geometry update message arrives.
if (!this._printTwipsMessagesForReplay) {
var ownViewTypes = this.isCalc() ? [
'cellcursor',
'referencemarks',
'cellselectionarea',
'textselection',
'invalidatecursor',
'textselectionstart',
'textselectionend',
'graphicselection',
] : [
'invalidatecursor',
'textselection',
'graphicselection'
];
if (this.isWriter())
ownViewTypes.push('contentcontrol');
var otherViewTypes = this.isCalc() ? [
'cellviewcursor',
'textviewselection',
'invalidateviewcursor',
'graphicviewselection',
] : [
'textviewselection',
'invalidateviewcursor'
];
this._printTwipsMessagesForReplay = new L.MessageStore(ownViewTypes, otherViewTypes);
}
var colonIndex = textMsg.indexOf(':');
if (colonIndex === -1) {
return;
}
var msgType = textMsg.substring(0, colonIndex);
this._printTwipsMessagesForReplay.save(msgType, textMsg, viewId);
},
_clearMsgReplayStore: function (notOtherMsg) {
if (!this._printTwipsMessagesForReplay) {
return;
}
this._printTwipsMessagesForReplay.clear(notOtherMsg);
},
_replayPrintTwipsMsgs: function (differentSheet) {
if (!this._printTwipsMessagesForReplay) {
return;
}
this._printTwipsMessagesForReplay.forEach(function (msg) {
// don't try and replace graphic selection if the sheet/page has changed
var skipMessage = differentSheet && msg.startsWith('graphicselection:');
if (!skipMessage)
this._onMessage(msg);
}.bind(this));
},
_replayPrintTwipsMsg: function (msgType) {
var msg = this._printTwipsMessagesForReplay.get(msgType);
this._onMessage(msg);
},
_replayPrintTwipsMsgAllViews: function (msgType) {
Object.keys(this._cellViewCursors).forEach(function (viewId) {
var msg = this._printTwipsMessagesForReplay.get(msgType, parseInt(viewId));
if (msg)
this._onMessage(msg);
}.bind(this));
},
_syncTilePanePos: function () {
var tilePane = this._container.parentElement;
if (tilePane) {
var mapPanePos = this._map._getMapPanePos();
L.DomUtil.setPosition(tilePane, new L.Point(-mapPanePos.x , -mapPanePos.y));
var documentBounds = this._map.getPixelBoundsCore();
var documentPos = documentBounds.min;
var documentEndPos = documentBounds.max;
this._painter._sectionContainer.setDocumentBounds([documentPos.x, documentPos.y, documentEndPos.x, documentEndPos.y]);
}
},
pauseDrawing: function () {
if (this._painter && this._painter._sectionContainer)
this._painter._sectionContainer.pauseDrawing();
},
resumeDrawing: function (topLevel) {
if (this._painter && this._painter._sectionContainer)
this._painter._sectionContainer.resumeDrawing(topLevel);
},
enableDrawing: function () {
if (this._painter && this._painter._sectionContainer)
this._painter._sectionContainer.enableDrawing();
},
_getUIWidth: function () {
var section = this._painter._sectionContainer.getSectionWithName(L.CSections.RowHeader.name);
if (section) {
return Math.round(section.size[0] / app.dpiScale);
}
else {
return 0;
}
},
_getUIHeight: function () {
var section = this._painter._sectionContainer.getSectionWithName(L.CSections.ColumnHeader.name);
if (section) {
return Math.round(section.size[1] / app.dpiScale);
}
else {
return 0;
}
},
_getGroupWidth: function () {
var section = this._painter._sectionContainer.getSectionWithName(L.CSections.RowGroup.name);
if (section) {
return Math.round(section.size[0] / app.dpiScale);
}
else {
return 0;
}
},
_getGroupHeight: function () {
var section = this._painter._sectionContainer.getSectionWithName(L.CSections.ColumnGroup.name);
if (section) {
return Math.round(section.size[1] / app.dpiScale);
}
else {
return 0;
}
},
_getTilesSectionRectangle: function () {
var section = app.sectionContainer.getSectionWithName(L.CSections.Tiles.name);
if (section) {
return L.LOUtil.createRectangle(section.myTopLeft[0] / app.dpiScale, section.myTopLeft[1] / app.dpiScale, section.size[0] / app.dpiScale, section.size[1] / app.dpiScale);
}
else {
return L.LOUtil.createRectangle(0, 0, 0, 0);
}
},
_getRealMapSize: function() {
this._map._sizeChanged = true; // force using real size
return this._map.getPixelBounds().getSize();
},
_syncTileContainerSize: function () {
if (this._docType === 'presentation' || this._docType === 'drawing') {
this.onResizeImpress();
}
var tileContainer = this._container;
if (tileContainer) {
var documentContainerSize = document.getElementById('document-container');
documentContainerSize = documentContainerSize.getBoundingClientRect();
documentContainerSize = [documentContainerSize.width, documentContainerSize.height];
this._painter._sectionContainer.onResize(documentContainerSize[0], documentContainerSize[1]); // Canvas's size = documentContainer's size.
var oldSize = this._getRealMapSize();
var rectangle = this._getTilesSectionRectangle();
var mapElement = document.getElementById('map'); // map's size = tiles section's size.
mapElement.style.left = rectangle.getPxX1() + 'px';
mapElement.style.top = rectangle.getPxY1() + 'px';
mapElement.style.width = rectangle.getPxWidth() + 'px';
mapElement.style.height = rectangle.getPxHeight() + 'px';
tileContainer.style.width = rectangle.getPxWidth() + 'px';
tileContainer.style.height = rectangle.getPxHeight() + 'px';
var newSize = this._getRealMapSize();
var heightIncreased = oldSize.y < newSize.y;
var widthIncreased = oldSize.x < newSize.x;
if (this._docType === 'spreadsheet') {
if (this._painter._sectionContainer.doesSectionExist(L.CSections.RowHeader.name)) {
this._painter._sectionContainer.getSectionWithName(L.CSections.RowHeader.name)._updateCanvas();
this._painter._sectionContainer.getSectionWithName(L.CSections.ColumnHeader.name)._updateCanvas();
}
}
if (oldSize.x !== newSize.x || oldSize.y !== newSize.y) {
this._map.invalidateSize();
}
var hasMobileWizardOpened = this._map.uiManager.mobileWizard ? this._map.uiManager.mobileWizard.isOpen() : false;
var hasIframeModalOpened = $('.iframe-dialog-modal').is(':visible');
// when integrator has opened dialog in parent frame (eg. save as) we shouldn't steal the focus
var focusedUI = document.activeElement === document.body;
if (window.mode.isMobile() && !hasMobileWizardOpened && !hasIframeModalOpened && !focusedUI) {
if (heightIncreased) {
// if the keyboard is hidden - be sure we setup correct state in TextInput
this._map.setAcceptInput(false);
} else
this._onUpdateCursor(true);
}
this._fitWidthZoom();
// Center the view w.r.t the new map-pane position using the current zoom.
this._map.setView(this._map.getCenter());
// We want to keep cursor visible when we show the keyboard on mobile device or tablet
var isTabletOrMobile = window.mode.isMobile() || window.mode.isTablet();
var hasVisibleCursor = app.file.textCursor.visible
&& this._map._docLayer._cursorMarker && this._map._docLayer._cursorMarker.isDomAttached();
if (!heightIncreased && isTabletOrMobile && this._map._docLoaded && hasVisibleCursor) {
var cursorPos = this._map._docLayer._twipsToLatLng({ x: app.file.textCursor.rectangle.x1, y: app.file.textCursor.rectangle.y2 });
var centerOffset = this._map._getCenterOffset(cursorPos);
var viewHalf = this._map.getSize()._divideBy(2);
var cursorPositionInView =
centerOffset.x > -viewHalf.x && centerOffset.x < viewHalf.x &&
centerOffset.y > -viewHalf.y && centerOffset.y < viewHalf.y;
if (!cursorPositionInView)
this._map.panTo(cursorPos);
}
if (heightIncreased || widthIncreased) {
this._painter._sectionContainer.requestReDraw();
this._map.fire('sizeincreased');
}
}
},
hasSplitPanesSupport: function () {
// Only enabled for Calc for now
// It may work without this.options.sheetGeometryDataEnabled but not tested.
// The overlay-pane with split-panes is still based on svg renderer,
// and not available for VML or canvas yet.
if (this.isCalc() &&
this.options.sheetGeometryDataEnabled &&
L.Browser.svg) {
return true;
}
return false;
},
setZoomChanged: function (zoomChanged) {
this._painter._sectionContainer.setZoomChanged(zoomChanged);
},
onAdd: function (map) {
this._tileWidthPx = this.options.tileSize;
this._tileHeightPx = this.options.tileSize;
this._initContainer();
this._getToolbarCommandsValues();
this._textCSelections = new CSelections(undefined, this._canvasOverlay,
this._selectionsDataDiv, this._map, false /* isView */, undefined, 'text');
this._cellCSelections = new CSelections(undefined, this._canvasOverlay,
this._selectionsDataDiv, this._map, false /* isView */, undefined, 'cell');
this._oleCSelections = new CSelections(undefined, this._canvasOverlay,
this._selectionsDataDiv, this._map, false /* isView */, undefined, 'ole');
this._references = new CReferences(this._canvasOverlay);
this._referencesAll = [];
// This layergroup contains all the layers corresponding to other's view
this._viewLayerGroup = new L.LayerGroup();
if (!app.isReadOnly()) {
map.addLayer(this._viewLayerGroup);
}
this._debug = map._debug;
this._searchResultsLayer = new L.LayerGroup();
map.addLayer(this._searchResultsLayer);
this._levels = {};
this._tiles = {}; // stores all tiles, keyed by coordinates, and cached, compressed deltas
app.socket.sendMessage('commandvalues command=.uno:AcceptTrackedChanges');
map._fadeAnimated = false;
this._viewReset();
map.on('drag resize zoomend', this._updateScrollOffset, this);
map.on('dragover', this._onDragOver, this);
map.on('drop', this._onDrop, this);
map.on('zoomstart', this._onZoomStart, this);
map.on('zoomend', this._onZoomEnd, this);
if (this._docType === 'spreadsheet') {
map.on('zoomend', this._onCellCursorShift, this);
}
map.on('zoomend', L.bind(this.eachView, this, this._viewCursors, this._onUpdateViewCursor, this, false));
map.on('dragstart', this._onDragStart, this);
map.on('error', this._mapOnError, this);
if (map.options.autoFitWidth !== false) {
// always true since autoFitWidth is never set
map.on('resize', this._fitWidthZoom, this);
}
// Retrieve the initial cell cursor position (as LOK only sends us an
// updated cell cursor when the selected cell is changed and not the initial
// cell).
map.on('statusindicator',
function (e) {
if (e.statusType === 'alltilesloaded' && this._docType === 'spreadsheet') {
if (!this._map.uiManager.isAnyDialogOpen())
this._onCellCursorShift(true);
}
},
this);
map.on('updatepermission', function(e) {
if (e.perm !== 'edit') {
this._clearSelections();
}
}, this);
for (var key in this._selectionHandles) {
this._selectionHandles[key].on('drag dragend', this._onSelectionHandleDrag, this);
}
this._cellResizeMarkerStart.on('dragstart drag dragend', this._onCellResizeMarkerDrag, this);
this._cellResizeMarkerEnd.on('dragstart drag dragend', this._onCellResizeMarkerDrag, this);
this._referenceMarkerStart.on('dragstart drag dragend', this._onReferenceMarkerDrag, this);
this._referenceMarkerEnd.on('dragstart drag dragend', this._onReferenceMarkerDrag, this);
map.setPermission(app.file.permission);
map.fire('statusindicator', {statusType: 'coolloaded'});
this._map.sendInitUNOCommands();
this._resetClientVisArea();
this._requestNewTiles();
map.setZoom();
// This is called when page size is increased
// the content of the page that become visible may stay empty
// unless we have the tiles in the cache already
// This will only fetch the tiles which are invalid or does not exist
map.on('sizeincreased', function() {
this._update();
}.bind(this));
},
onRemove: function (map) {
this._painter.dispose();
L.DomUtil.remove(this._container);
map._removeZoomLimit(this);
this._container = null;
this._tileZoom = null;
this._clearPreFetch();
clearTimeout(this._previewInvalidator);
if (!this._cellCSelections.empty()) {
this._cellCSelections.clear();
}
if (!this._textCSelections.empty()) {
this._textCSelections.clear();
}
if (!this._oleCSelections.empty()) {
this._oleCSelections.clear();
}
if (this._cursorMarker && this._cursorMarker.isDomAttached()) {
this._cursorMarker.remove();
}
if (this._graphicMarker) {
this._graphicMarker.remove();
}
for (var key in this._selectionHandles) {
this._selectionHandles[key].remove();
}
this._removeSplitters();
L.DomUtil.remove(this._canvasContainer);
},
getEvents: function () {
var events = {
viewreset: this._viewReset,
movestart: this._moveStart,
// update tiles on move, but not more often than once per given interval
move: L.Util.throttle(this._move, this.options.updateInterval, this),
moveend: this._moveEnd,
splitposchanged: this._move,
};
return events;
},
// zoom is the new intermediate zoom level (log scale : 1 to 14)
zoomStep: function (zoom, newCenter) {
this._painter.zoomStep(zoom, newCenter);
},
zoomStepEnd: function (zoom, newCenter, mapUpdater, runAtFinish, noGap) {
this._painter.zoomStepEnd(zoom, newCenter, mapUpdater, runAtFinish, noGap);
},
preZoomAnimation: function (pinchStartCenter) {
this._pinchStartCenter = this._map.project(pinchStartCenter).multiplyBy(app.dpiScale); // in core pixels
if (this.isCursorVisible()) {
this._cursorMarker.setOpacity(0);
}
if (this._map._textInput._cursorHandler) {
this._map._textInput._cursorHandler.setOpacity(0);
}
if (this._cellCursorMarker) {
this._map.setOverlaysOpacity(0);
this._map.setMarkersOpacity(0);
}
if (this._selectionHandles['start']) {
this._selectionHandles['start'].setOpacity(0);
}
if (this._selectionHandles['end']) {
this._selectionHandles['end'].setOpacity(0);
}
this.eachView(this._viewCursors, function (item) {
var viewCursorMarker = item.marker;
if (viewCursorMarker) {
viewCursorMarker.setOpacity(0);
}
}, this, true);
},
postZoomAnimation: function () {
if (this.isCursorVisible()) {
this._cursorMarker.setOpacity(1);
}
if (this._map._textInput._cursorHandler) {
this._map._textInput._cursorHandler.setOpacity(1);
}
if (this._cellCursorMarker) {
this._map.setOverlaysOpacity(1);
this._map.setMarkersOpacity(1);
}
if (this._selectionHandles['start']) {
this._selectionHandles['start'].setOpacity(1);
}
if (this._selectionHandles['end']) {
this._selectionHandles['end'].setOpacity(1);
}
if (this._annotations) {
var annotations = this._annotations;
if (annotations.update)
setTimeout(function() {
annotations.update();
}, 250 /* ms */);
}
},
// Meant for desktop case, where the ending zoom and centers are all known in advance.
runZoomAnimation: function (zoomEnd, pinchCenter, mapUpdater, runAtFinish) {
this.preZoomAnimation(pinchCenter);
this.zoomStep(this._map.getZoom(), pinchCenter);
var thisObj = this;
this.zoomStepEnd(zoomEnd, pinchCenter,
mapUpdater,
// runAtFinish
function () {
thisObj.postZoomAnimation();
runAtFinish();
});
},
_viewReset: function (e) {
this._reset(e && e.hard);
if (this._docType === 'spreadsheet' && this._annotations !== 'undefined') {
app.socket.sendMessage('commandvalues command=.uno:ViewAnnotationsPosition');
}
},
_removeSplitters: function () {
if (this._xSplitter) {
this._canvasOverlay.removePath(this._xSplitter);
this._xSplitter = undefined;
}
if (this._ySplitter) {
this._canvasOverlay.removePath(this._ySplitter);
this._ySplitter = undefined;
}
},
_updateOpacity: function () {
this._pruneTiles();
},
_pruneTiles: function () {
// update tile.current for the view
if (app.file.fileBasedView)
this._updateFileBasedView(true);
this._garbageCollect();
},
_getTilePos: function (coords) {
return coords.getPos();
},
_pxBoundsToTileRanges: function (bounds) {
if (!this._splitPanesContext) {
return [this._pxBoundsToTileRange(bounds)];
}
var boundList = this._splitPanesContext.getPxBoundList(bounds);
return boundList.map(this._pxBoundsToTileRange, this);
},
_pxBoundsToTileRange: function (bounds) {
return new L.Bounds(
bounds.min.divideBy(this._tileSize).floor(),
bounds.max.divideBy(this._tileSize).floor());
},
_corePixelsToCss: function (corePixels) {
return corePixels.divideBy(app.dpiScale);
},
_cssPixelsToCore: function (cssPixels) {
return cssPixels.multiplyBy(app.dpiScale);
},
_cssBoundsToCore: function (bounds) {
return new L.Bounds(
this._cssPixelsToCore(bounds.min),
this._cssPixelsToCore(bounds.max)
);
},
_twipsToCorePixels: function (twips) {
return new L.Point(
twips.x / this._tileWidthTwips * this._tileSize,
twips.y / this._tileHeightTwips * this._tileSize);
},
_twipsToCorePixelsBounds: function (twips) {
return new L.Bounds(
this._twipsToCorePixels(twips.min),
this._twipsToCorePixels(twips.max)
);
},
_corePixelsToTwips: function (corePixels) {
return new L.Point(
corePixels.x / this._tileSize * this._tileWidthTwips,
corePixels.y / this._tileSize * this._tileHeightTwips);
},
_twipsToCssPixels: function (twips) {
return new L.Point(
(twips.x / this._tileWidthTwips) * (this._tileSize / app.dpiScale),
(twips.y / this._tileHeightTwips) * (this._tileSize / app.dpiScale));
},
_cssPixelsToTwips: function (pixels) {
return new L.Point(
((pixels.x * app.dpiScale) / this._tileSize) * this._tileWidthTwips,
((pixels.y * app.dpiScale) / this._tileSize) * this._tileHeightTwips);
},
_twipsToLatLng: function (twips, zoom) {
var pixels = this._twipsToCssPixels(twips);
return this._map.unproject(pixels, zoom);
},
_latLngToTwips: function (latLng, zoom) {
var pixels = this._map.project(latLng, zoom);
return this._cssPixelsToTwips(pixels);
},
_latLngToCorePixels: function(latLng, zoom) {
var pixels = this._map.project(latLng, zoom);
return new L.Point (
pixels.x * app.dpiScale,
pixels.y * app.dpiScale);
},
_twipsToPixels: function (twips) { // css pixels
return this._twipsToCssPixels(twips);
},
_pixelsToTwips: function (pixels) { // css pixels
return this._cssPixelsToTwips(pixels);
},
_twipsToCoords: function (twips) {
return new L.TileCoordData(
Math.round(twips.x / twips.tileWidth) * this._tileSize,
Math.round(twips.y / twips.tileHeight) * this._tileSize);
},
_coordsToTwips: function (coords) {
return new L.Point(
Math.floor(coords.x / this._tileSize) * this._tileWidthTwips,
Math.floor(coords.y / this._tileSize) * this._tileHeightTwips);
},
_isValidTile: function (coords) {
if (coords.x < 0 || coords.y < 0) {
return false;
}
else if ((coords.x / this._tileSize) * this._tileWidthTwips > this._docWidthTwips ||
(coords.y / this._tileSize) * this._tileHeightTwips > this._docHeightTwips) {
return false;
}
else
return true;
},
_updateMaxBounds: function (sizeChanged) {
if (this._docWidthTwips === undefined || this._docHeightTwips === undefined) {
return;
}
var docPixelLimits = new L.Point(app.file.size.pixels[0] / app.dpiScale, app.file.size.pixels[1] / app.dpiScale);
var scrollPixelLimits = new L.Point(app.view.size.pixels[0] / app.dpiScale, app.view.size.pixels[1] / app.dpiScale);
var topLeft = this._map.unproject(new L.Point(0, 0));
if (this._documentInfo === '' || sizeChanged) {
// we just got the first status so we need to center the document
this._map.setDocBounds(new L.LatLngBounds(topLeft, this._map.unproject(docPixelLimits)));
this._map.setMaxBounds(new L.LatLngBounds(topLeft, this._map.unproject(scrollPixelLimits)));
}
this._docPixelSize = {x: docPixelLimits.x, y: docPixelLimits.y};
this._map.fire('scrolllimits', {x: scrollPixelLimits.x, y: scrollPixelLimits.y});
},
// Used with filebasedview.
_getMostVisiblePart: function (queue) {
var parts = [];
var found = false;
for (var i = 0; i < queue.length; i++) {
for (var j = 0; j < parts.length; j++) {
if (parts[j].part === queue[i].part) {
found = true;
break;
}
}
if (!found)
parts.push({part: queue[i].part});
found = false;
}
var ratio = this._tileSize / this._tileHeightTwips;
var partHeightPixels = Math.round((this._partHeightTwips + this._spaceBetweenParts) * ratio);
var partWidthPixels = Math.round(this._partWidthTwips * ratio);
var rectangle;
var maxArea = -1;
var mostVisiblePart = 0;
var docBoundsRectangle = app.sectionContainer.getDocumentBounds();
docBoundsRectangle[2] = docBoundsRectangle[2] - docBoundsRectangle[0];
docBoundsRectangle[3] = docBoundsRectangle[3] - docBoundsRectangle[1];
for (i = 0; i < parts.length; i++) {
rectangle = [0, partHeightPixels * parts[i].part, partWidthPixels, partHeightPixels];
rectangle = L.LOUtil._getIntersectionRectangle(rectangle, docBoundsRectangle);
if (rectangle) {
if (rectangle[2] * rectangle[3] > maxArea) {
maxArea = rectangle[2] * rectangle[3];
mostVisiblePart = parts[i].part;
}
}
}
return mostVisiblePart;
},
_sortFileBasedQueue: function (queue) {
for (var i = 0; i < queue.length - 1; i++) {
for (var j = i + 1; j < queue.length; j++) {
var a = queue[i];
var b = queue[j];
var switchTiles = false;
if (a.part === b.part) {
if (a.y > b.y) {
switchTiles = true;
}
else if (a.y === b.y) {
switchTiles = a.x > b.x;
}
else {
switchTiles = false;
}
}
else {
switchTiles = a.part > b.part;
}
if (switchTiles) {
var temp = a;
queue[i] = b;
queue[j] = temp;
}
}
}
},
highlightCurrentPart: function (part) {
var previews = document.getElementsByClassName('preview-frame');
for (var i = 0; i < previews.length; i++) {
if (parseInt(previews[i].id.replace('preview-frame-part-', '')) === part) {
previews[i].style.border = '2px solid darkgrey';
}
else {
previews[i].style.border = 'none';
}
}
},
// Used with file based view. Check the most visible part and set the selected part if needed.
_checkSelectedPart: function () {
var queue = this._updateFileBasedView(true);
if (queue.length > 0) {
var partToSelect = this._getMostVisiblePart(queue);
if (this._selectedPart !== partToSelect) {
this._selectedPart = partToSelect;
this._preview._scrollToPart();
this.highlightCurrentPart(partToSelect);
app.socket.sendMessage('setclientpart part=' + this._selectedPart);
}
}
},
_updateFileBasedView: function (checkOnly, zoomFrameBounds, forZoom) {
if (this._partHeightTwips === 0) // This is true before status message is handled.
return [];
if (this._isZooming)
return [];
if (!checkOnly) {
// zoomFrameBounds and forZoom params were introduced to work only in checkOnly mode.
window.app.console.assert(zoomFrameBounds === undefined, 'zoomFrameBounds must only be supplied when checkOnly is true');
window.app.console.assert(forZoom === undefined, 'forZoom must only be supplied when checkOnly is true');
}
if (forZoom !== undefined) {
window.app.console.assert(zoomFrameBounds, 'zoomFrameBounds must be valid when forZoom is specified');
}
var zoom = forZoom || Math.round(this._map.getZoom());
var currZoom = Math.round(this._map.getZoom());
var relScale = currZoom == zoom ? 1 : this._map.getZoomScale(zoom, currZoom);
var ratio = this._tileSize * relScale / this._tileHeightTwips;
var partHeightPixels = Math.round((this._partHeightTwips + this._spaceBetweenParts) * ratio);
var partWidthPixels = Math.round((this._partWidthTwips) * ratio);
var mode = 0; // mode is different only in Impress MasterPage mode so far
var intersectionAreaRectangle = L.LOUtil._getIntersectionRectangle(app.file.viewedRectangle.pToArray(), [0, 0, partWidthPixels, partHeightPixels * this._parts]);
var queue = [];
if (intersectionAreaRectangle) {
var minLocalX = Math.floor(intersectionAreaRectangle[0] / app.tile.size.pixels[0]) * app.tile.size.pixels[0];
var maxLocalX = Math.floor((intersectionAreaRectangle[0] + intersectionAreaRectangle[2]) / app.tile.size.pixels[0]) * app.tile.size.pixels[0];
var startPart = Math.floor(intersectionAreaRectangle[1] / partHeightPixels);
var startY = app.file.viewedRectangle.pY1 - startPart * partHeightPixels;
startY = Math.floor(startY / app.tile.size.pixels[1]) * app.tile.size.pixels[1];
var endPart = Math.ceil((intersectionAreaRectangle[1] + intersectionAreaRectangle[3]) / partHeightPixels);
var endY = app.file.viewedRectangle.pY1 + app.file.viewedRectangle.pY2 - endPart * partHeightPixels;
endY = Math.floor(endY / app.tile.size.pixels[1]) * app.tile.size.pixels[1];
var vTileCountPerPart = Math.ceil(partHeightPixels / app.tile.size.pixels[1]);
for (var i = startPart; i < endPart; i++) {
for (var j = minLocalX; j <= maxLocalX; j += app.tile.size.pixels[0]) {
for (var k = 0; k <= vTileCountPerPart * app.tile.size.pixels[0]; k += app.tile.size.pixels[1])
if ((i !== startPart || k >= startY) && (i !== endPart || k <= endY))
queue.push(new L.TileCoordData(j, k, zoom, i, mode));
}
}
this._sortFileBasedQueue(queue);
for (i = 0; i < this._tiles.length; i++) {
this._tiles[i].current = false; // Visible ones's "current" property will be set to true below.
}
for (i = 0; i < queue.length; i++) {
var tempTile = this._tiles[this._tileCoordsToKey(queue[i])];
if (tempTile)
tempTile.current = true;
}
}
if (checkOnly) {
return queue;
}
else {
this._sendClientVisibleArea();
this._sendClientZoom();
var tileCombineQueue = [];
for (var i = 0; i < queue.length; i++) {
var key = this._tileCoordsToKey(queue[i]);
var tile = this._tiles[key];
if (!tile)
tile = this.createTile(queue[i], key);
if (tile.needsFetch())
tileCombineQueue.push(queue[i]);
}
this._sendTileCombineRequest(tileCombineQueue);
}
},
_getMissingTiles: function (pixelBounds, zoom) {
var tileRanges = this._pxBoundsToTileRanges(pixelBounds);
var queue = [];
// create a queue of coordinates to load tiles from
for (var rangeIdx = 0; rangeIdx < tileRanges.length; ++rangeIdx) {
var tileRange = tileRanges[rangeIdx];
for (var j = tileRange.min.y; j <= tileRange.max.y; ++j) {
for (var i = tileRange.min.x; i <= tileRange.max.x; ++i) {
var coords = new L.TileCoordData(
i * this._tileSize,
j * this._tileSize,
zoom,
this._selectedPart,
this._selectedMode);
if (!this._isValidTile(coords)) { continue; }
var key = this._tileCoordsToKey(coords);
var tile = this._tiles[key];
if (tile && !tile.needsFetch())
tile.current = true;
else
queue.push(coords);
}
}
}
return queue;
},
_update: function (center, zoom) {
var map = this._map;
if (!map || this._documentInfo === '' || !this._canonicalIdInitialized) {
return;
}
// Calc: do not set view area too early after load and before we get the cursor position.
if (this.isCalc() && !this._gotFirstCellCursor)
return;
// be sure canvas is initialized already and has correct size
var size = map.getSize();
if (size.x === 0 || size.y === 0) {
setTimeout(function () { this._update(); }.bind(this), 1);
return;
}
if (app.file.fileBasedView) {
this._updateFileBasedView();
return;
}
if (center === undefined) { center = map.getCenter(); }
if (zoom === undefined) { zoom = Math.round(map.getZoom()); }
for (var key in this._tiles) {
var thiscoords = this._keyToTileCoords(key);
if (thiscoords.z !== zoom ||
thiscoords.part !== this._selectedPart ||
thiscoords.mode !== this._selectedMode) {
this._tiles[key].current = false;
}
}
var pixelBounds = map.getPixelBoundsCore(center, zoom);
var queue = this._getMissingTiles(pixelBounds, zoom);
this._sendClientVisibleArea();
this._sendClientZoom();
if (queue.length !== 0)
this._addTiles(queue, false);
if (this.isCalc() || this.isWriter())
this._initPreFetchAdjacentTiles(pixelBounds, zoom);
},
_initPreFetchAdjacentTiles: function (pixelBounds, zoom) {
if (this._adjacentTilePreFetcher)
clearTimeout(this._adjacentTilePreFetcher);
this._adjacentTilePreFetcher = setTimeout(function() {
// Extend what we request to include enough to populate a full
// scroll after or before the current viewport
//
// request separately from the current viewPort to get
// those tiles first.
var pixelHeight = pixelBounds.getSize().y;
var pixelPrevNextHeight = pixelHeight;
var pixelTopLeft = pixelBounds.getTopLeft();
var pixelBottomRight = pixelBounds.getBottomRight();
if (this.isCalc())
pixelPrevNextHeight = ~~ (pixelPrevNextHeight * 1.5);
pixelTopLeft.y += pixelHeight;
pixelBottomRight.y += pixelPrevNextHeight;
pixelBounds = new L.Bounds(pixelTopLeft, pixelBottomRight);
var queue = this._getMissingTiles(pixelBounds, zoom);
pixelTopLeft.y -= pixelHeight + pixelPrevNextHeight;
pixelBottomRight.y -= pixelHeight + pixelPrevNextHeight;
pixelBounds = new L.Bounds(pixelTopLeft, pixelBottomRight);
queue = queue.concat(this._getMissingTiles(pixelBounds, zoom));
if (queue.length !== 0)
this._addTiles(queue, true);
}.bind(this), 250 /*ms*/);
},
_sendClientVisibleArea: function (forceUpdate) {
if (!this._map._docLoaded)
return;
var splitPos = this._splitPanesContext ? this._splitPanesContext.getSplitPos() : new L.Point(0, 0);
var visibleArea = this._map.getPixelBounds();
visibleArea = new L.Bounds(
this._pixelsToTwips(visibleArea.min),
this._pixelsToTwips(visibleArea.max)
);
splitPos = this._corePixelsToTwips(splitPos);
var size = visibleArea.getSize();
var visibleTopLeft = visibleArea.min;
var newClientVisibleArea = 'clientvisiblearea x=' + Math.round(visibleTopLeft.x)
+ ' y=' + Math.round(visibleTopLeft.y)
+ ' width=' + Math.round(size.x)
+ ' height=' + Math.round(size.y)
+ ' splitx=' + Math.round(splitPos.x)
+ ' splity=' + Math.round(splitPos.y);
if (this._ySplitter) {
this._ySplitter.onPositionChange();
}
if (this._xSplitter) {
this._xSplitter.onPositionChange();
}
if (this._clientVisibleArea !== newClientVisibleArea || forceUpdate) {
// Visible area is dirty, update it on the server
app.socket.sendMessage(newClientVisibleArea);
if (!this._map._fatal && app.idleHandler._active && app.socket.connected())
this._clientVisibleArea = newClientVisibleArea;
if (this._debug.tileInvalidationsOn)
this._debug._tileInvalidationLayer.clearLayers();
}
},
_updateOnChangePart: function () {
var map = this._map;
if (!map || this._documentInfo === '') {
return;
}
var key, coords, tile;
var center = map.getCenter();
var zoom = Math.round(map.getZoom());
var pixelBounds = map.getPixelBoundsCore(center, zoom);
var tileRanges = this._pxBoundsToTileRanges(pixelBounds);
var queue = [];
// mark tiles not matching our part & mode as not being current
for (key in this._tiles) {
var thiscoords = this._keyToTileCoords(key);
if (thiscoords.z !== zoom ||
thiscoords.part !== this._selectedPart ||
thiscoords.mode !== this._selectedMode) {
this._tiles[key].current = false;
}
}
// create a queue of coordinates to load tiles from
for (var rangeIdx = 0; rangeIdx < tileRanges.length; ++rangeIdx) {
var tileRange = tileRanges[rangeIdx];
for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
coords = new L.TileCoordData(
i * this._tileSize,
j * this._tileSize,
zoom,
this._selectedPart,
this._selectedMode);
if (!this._isValidTile(coords)) { continue; }
key = this._tileCoordsToKey(coords);
tile = this._tiles[key];
if (tile && !tile.needsFetch())
tile.current = true;
else
queue.push(coords);
}
}
}
if (queue.length !== 0) {
var tileCombineQueue = [];
for (i = 0; i < queue.length; i++) {
coords = queue[i];
key = this._tileCoordsToKey(coords);
if (!this._tiles[key])
this.createTile(coords, key);
if (this._tileNeedsFetch(key)) {
tileCombineQueue.push(coords);
}
}
if (tileCombineQueue.length >= 0) {
this._sendTileCombineRequest(tileCombineQueue);
} else {
// We have all necessary tile images in the cache, schedule a paint..
// This may not be immediate if we are now in a slurp events call.
this._painter.update();
}
}
if (this._docType === 'presentation' || this._docType === 'drawing')
this._initPreFetchPartTiles();
},
_tileReady: function (coords) {
var key = this._tileCoordsToKey(coords);
var tile = this._tiles[key];
if (!tile)
return;
var emptyTilesCountChanged = false;
if (this._emptyTilesCount > 0) {
this._emptyTilesCount -= 1;
emptyTilesCountChanged = true;
}
if (this._map && emptyTilesCountChanged && this._emptyTilesCount === 0) {
this._map.fire('statusindicator', { statusType: 'alltilesloaded' });
}
var now = new Date();
// Newly (pre)-fetched tiles, rendered or not should be privileged.
tile.lastRendered = now;
// Don't paint the tile, only dirty the sectionsContainer if it is in the visible area.
// _emitSlurpedTileEvents() will repaint canvas (if it is dirty).
if (this._painter.coordsIntersectVisible(coords)) {
this._painter._sectionContainer.setDirty(coords);
}
},
// create tiles if needed for queued coordinates, and build a
// tilecombined request for any tiles we need to fetch.
_addTiles: function (coordsQueue, preFetch) {
var coords, key;
for (var i = 0; i < coordsQueue.length; i++) {
coords = coordsQueue[i];
key = this._tileCoordsToKey(coords);
if (coords.part === this._selectedPart &&
coords.mode === this._selectedMode) {
var tile = this._tiles[key];
if (!tile) {
// We always want to ensure the tile
// exists.
tile = this.createTile(coords, key);
}
if (preFetch) {
// If preFetching at idle, take the
// opportunity to create an up to date
// canvas for the tile in advance.
this.ensureCanvas(tile, null, true);
}
}
}
// sort the tiles by the rows
coordsQueue.sort(function (a, b) {
if (a.y !== b.y) {
return a.y - b.y;
} else {
return a.x - b.x;
}
});
// try group the tiles into rectangular areas
var rectangles = [];
while (coordsQueue.length > 0) {
coords = coordsQueue[0];
// tiles that do not interest us
key = this._tileCoordsToKey(coords);
if (!this._tileNeedsFetch(key)
|| coords.part !== this._selectedPart
|| coords.mode !== this._selectedMode) {
coordsQueue.splice(0, 1);
continue;
}
var rectQueue = [coords];
var bound = coords.getPos(); // L.Point
// remove it
coordsQueue.splice(0, 1);
// find the close ones
var rowLocked = false;
var hasHole = false;
i = 0;
while (i < coordsQueue.length) {
var current = coordsQueue[i];
// extend the bound vertically if possible (so far it was
// continuous)
if (!hasHole && (current.y === bound.y + this._tileSize)) {
rowLocked = true;
bound.y += this._tileSize;
}
if (current.y > bound.y) {
break;
}
if (!rowLocked) {
if (current.y === bound.y && current.x === bound.x + this._tileSize) {
// extend the bound horizontally
bound.x += this._tileSize;
rectQueue.push(current);
coordsQueue.splice(i, 1);
} else {
// ignore the rest of the row
rowLocked = true;
++i;
}
} else if (current.x <= bound.x && current.y <= bound.y) {
// we are inside the bound
rectQueue.push(current);
coordsQueue.splice(i, 1);
} else {
// ignore this one, but there still may be other tiles
hasHole = true;
++i;
}
}
rectangles.push(rectQueue);
}
for (var r = 0; r < rectangles.length; ++r)
this._sendTileCombineRequest(rectangles[r]);
if (this._docType === 'presentation' || this._docType === 'drawing')
this._initPreFetchPartTiles();
},
_checkTileMsgObject: function (msgObj) {
if (typeof msgObj !== 'object' ||
typeof msgObj.x !== 'number' ||
typeof msgObj.y !== 'number' ||
typeof msgObj.tileWidth !== 'number' ||
typeof msgObj.tileHeight !== 'number' ||
typeof msgObj.part !== 'number' ||
(typeof msgObj.mode !== 'number' && typeof msgObj.mode !== 'undefined')) {
window.app.console.error('Unexpected content in the parsed tile message.');
}
},
_tileMsgToCoords: function (tileMsg) {
var coords = this._twipsToCoords(tileMsg);
coords.z = tileMsg.zoom;
coords.part = tileMsg.part;
coords.mode = tileMsg.mode !== undefined ? tileMsg.mode : 0;
return coords;
},
_tileCoordsToKey: function (coords) {
return coords.key();
},
_keyToTileCoords: function (key) {
return L.TileCoordData.parseKey(key);
},
// Fix for cool#5876 allow immediate reuse of canvas context memory
// WKWebView has a hard limit on the number of bytes of canvas
// context memory that can be allocated. Reducing the canvas
// size to zero is a way to reduce the number of bytes counted
// against this limit.
_reclaimTileCanvasMemory: function (tile) {
if (tile && tile.canvas) {
tile.canvas.width = 0;
tile.canvas.height = 0;
delete tile.canvas;
}
tile.imgDataCache = null;
},
_removeTile: function (key) {
var tile = this._tiles[key];
if (!tile)
return;
if (!tile.hasContent() && this._emptyTilesCount > 0)
this._emptyTilesCount -= 1;
this._reclaimTileCanvasMemory(tile);
delete this._tiles[key];
},
// We keep tile content around, but it will need
// refreshing if we show it again - and we need to
// know what monotonic time the invalidate came from
// so we match this to a new incoming tile to unset
// the invalid state later.
_invalidateTile: function (key, wireId) {
var tile = this._tiles[key];
if (!tile)
return;
tile.invalidateCount++;
if (this._debug.tileDataOn) {
this._debug.tileDataAddInvalidate();
}
if (!tile.hasContent())
this._removeTile(key);
else
{
if (this._debugDeltas)
window.app.console.debug('invalidate tile ' + key + ' with wireId ' + wireId);
if (wireId)
tile.invalidFrom = wireId;
else
tile.invalidFrom = tile.wireId;
}
},
_prefetchTilesSync: function () {
if (!this._prefetcher)
this._prefetcher = new L.TilesPreFetcher(this, this._map);
this._prefetcher.preFetchTiles(true /* forceBorderCalc */, true /* immediate */);
},
_preFetchTiles: function (forceBorderCalc) {
if (this._prefetcher) {
this._prefetcher.preFetchTiles(forceBorderCalc);
}
},
_resetPreFetching: function (resetBorder) {
if (!this._prefetcher) {
this._prefetcher = new L.TilesPreFetcher(this, this._map);
}
this._prefetcher.resetPreFetching(resetBorder);
},
_clearPreFetch: function () {
if (this._prefetcher) {
this._prefetcher.clearPreFetch();
}
},
_clearTilesPreFetcher: function () {
if (this._prefetcher) {
this._prefetcher.clearTilesPreFetcher();
}
},
// Ensure we have a renderable canvas for a given tile
// Use this immediately before drawing a tile, pass in the time.
ensureCanvas: function(tile, now, forPrefetch)
{
if (!tile)
return;
if (!tile.canvas)
{
// This allocation is usually cheap and reliable,
// getting the canvas context, not so much.
var canvas = document.createElement('canvas');
canvas.width = window.tileSize;
canvas.height = window.tileSize;
tile.canvas = canvas;
// re-hydrate recursively from cached data
if (tile.hasKeyframe())
{
if (this._debugDeltas)
window.app.console.log('Restoring a tile from cached delta at ' +
this._tileCoordsToKey(tile.coords));
this._applyDelta(tile, tile.rawDeltas, true, false);
}
}
if (!forPrefetch)
{
if (now !== null)
tile.lastRendered = now;
if (!tile.hasContent())
tile.missingContent++;
}
},
_maybeGarbageCollect: function() {
if (!(++this._gcCounter % 53))
this._garbageCollect();
},
// FIXME: could trim quite hard here, and do this at idle ...
// Set a high and low watermark of how many canvases we want
// and expire old ones
_garbageCollect: function() {
// 4k screen -> 8Mpixel, each tile is 64kpixel uncompressed
var highNumCanvases = 250; // ~60Mb.
var lowNumCanvases = 125; // ~30Mb
// real RAM sizes for keyframes + delta cache in memory.
var highDeltaMemory = 120 * 1024 * 1024; // 120Mb
var lowDeltaMemory = 60 * 1024 * 1024; // 60Mb
// number of tiles
var highTileCount = 2 * 1024;
var lowTileCount = 1024;
if (this._debugDeltas)
window.app.console.log('Garbage collect! iter: ' + this._gcCounter);
/* uncomment to exercise me harder. */
/* highNumCanvases = 3; lowNumCanvases = 2;
highDeltaMemory = 1024*1024; lowDeltaMemory = 1024*128;
highTileCount = 100; lowTileCount = 50; */
var keys = [];
for (var key in this._tiles) // no .keys() method.
keys.push(key);
// FIXME: should we sort by wireId - which is monotonic server ~time
// sort by oldest
keys.sort(function(a,b) { return b.lastRendered - a.lastRendered; });
var canvasKeys = [];
var totalSize = 0;
for (var i = 0; i < keys.length; ++i)
{
var tile = this._tiles[keys[i]];
if (tile.canvas)
canvasKeys.push(keys[i]);
totalSize += tile.rawDeltas ? tile.rawDeltas.length : 0;
}
// Trim ourselves down to size.
if (canvasKeys.length > highNumCanvases)
{
for (var i = 0; i < canvasKeys.length - lowNumCanvases; ++i)
{
var key = canvasKeys[i];
var tile = this._tiles[key];
if (this._debugDeltas)
window.app.console.log('Reclaim canvas ' + key +
' last rendered: ' + tile.lastRendered);
this._reclaimTileCanvasMemory(tile);
}
}
// Trim memory down to size.
if (totalSize > highDeltaMemory)
{
for (var i = 0; i < keys.length && totalSize > lowDeltaMemory; ++i)
{
var key = keys[i];
var tile = this._tiles[key];
if (tile.rawDeltas && !tile.current)
{
totalSize -= tile.rawDeltas.length;
if (this._debugDeltas)
window.app.console.log('Reclaim delta ' + key + ' memory: ' +
tile.rawDeltas.length + ' bytes');
this._reclaimTileCanvasMemory(tile);
tile.rawDeltas = null;
// force keyframe
tile.wireId = 0;
tile.invalidFrom = 0;
}
}
}
// Trim the number of tiles down too ...
if (keys.length > highTileCount)
{
for (var i = 0; i < keys.length - lowTileCount; ++i)
{
var key = keys[i];
var tile = this._tiles[key];
if (!tile.current)
this._removeTile(keys[i]);
}
}
},
// work hard to ensure we get a canvas context to render with
_ensureContext: function(tile)
{
var ctx;
this._maybeGarbageCollect();
// important this is after the garbagecollect
if (!tile.canvas)
this.ensureCanvas(tile, null, false);
if ((ctx = tile.canvas.getContext('2d')))
return ctx;
// Not a good result - we ran out of canvas memory
this._garbageCollect();
if (!tile.canvas)
this.ensureCanvas(tile, null, false);
if ((ctx = tile.canvas.getContext('2d')))
return ctx;
// Free non-current canvas' and start again.
if (this._debugDeltas)
window.app.console.log('Free non-current tiles canvas memory');
for (var key in this._tiles) {
var t = this._tiles[key];
if (t && !t.current)
this._reclaimTileCanvasMemory(t);
}
if (!tile.canvas)
this.ensureCanvas(tile, null, false);
if ((ctx = tile.canvas.getContext('2d')))
return ctx;
if (this._debugDeltas)
window.app.console.log('Throw everything overbarod to free all tiles canvas memory');
for (var key in this._tiles) {
var t = this._tiles[key];
this._reclaimTileCanvasMemory(t);
}
if (!tile.canvas)
this.ensureCanvas(tile, null, false);
ctx = tile.canvas.getContext('2d');
if (!ctx)
window.app.console.log('Error: out of canvas memory.');
return ctx;
},
_unpremultiply: function(rawDelta, byteLength) {
var len = byteLength / 4;
var delta32 = new Uint32Array(rawDelta.buffer, rawDelta.byteOffset, len);
var resultu32 = new Uint32Array(len);
var resultu8 = new Uint8ClampedArray(resultu32.buffer, resultu32.byteOffset, resultu32.byteLength);
for (var i32 = 0; i32 < len; ++i32) {
// premultiplied rgba -> unpremultiplied rgba
var alpha = delta32[i32] >>> 24;
if (alpha === 255) {
resultu32[i32] = delta32[i32];
}
else if (alpha !== 0) { // dest can remain at ctored 0 if alpha is 0
var i8 = i32 * 4;
// forced to do the math
resultu8[i8] = Math.ceil(rawDelta[i8] * 255 / alpha);
resultu8[i8 + 1] = Math.ceil(rawDelta[i8 + 1] * 255 / alpha);
resultu8[i8 + 2] = Math.ceil(rawDelta[i8 + 2] * 255 / alpha);
resultu8[i8 + 3] = alpha;
}
}
return resultu8;
},
_applyDelta: function(tile, rawDelta, isKeyframe, wireMessage) {
// 'Uint8Array' rawDelta
if (this._debugDeltas)
window.app.console.log('Applying a raw ' + (isKeyframe ? 'keyframe' : 'delta') +
' of length ' + rawDelta.length +
(this._debugDeltasDetail ? (' hex: ' + hex2string(rawDelta)) : ''));
// Important to recurse & re-constitute from tile.rawDeelts
// before appending rawDelta and then applying it again.
var ctx = this._ensureContext(tile);
if (!ctx) // out of canvas / texture memory.
return;
// if re-creating a canvas from rawDeltas don't update counts
if (wireMessage) {
if (isKeyframe) {
tile.loadCount++;
tile.deltaCount = 0;
tile.updateCount = 0;
if (this._debug.tileDataOn) {
this._debug.tileDataAddLoad();
}
} else if (rawDelta.length === 0) {
tile.updateCount++;
this._nullDeltaUpdate++;
if (this._emptyDeltaDiv) {
this._emptyDeltaDiv.innerText = this._nullDeltaUpdate;
}
if (this._debug.tileDataOn) {
this._debug.tileDataAddUpdate();
}
return; // that was easy
} else {
tile.deltaCount++;
if (this._debug.tileDataOn) {
this._debug.tileDataAddDelta();
}
}
}
// else - re-constituting from tile.rawData
var traceEvent = app.socket.createCompleteTraceEvent('L.CanvasTileLayer.applyDelta',
{ keyFrame: isKeyframe, length: rawDelta.length });
// store the compressed version for later in its current
// form as byte arrays, so that we can manage our canvases
// better.
if (isKeyframe)
{
if (tile.rawDeltas && tile.rawDeltas != rawDelta) // help the gc?
tile.rawDeltas.length = 0;
tile.rawDeltas = rawDelta; // overwrite
}
else if (!tile.rawDeltas)
{
window.app.console.log('Unusual: attempt to append a delta when we have no keyframe.');
return;
}
else // assume we already have a delta.
{
// FIXME: this is not beautiful; but no concatenate here.
var tmp = new Uint8Array(tile.rawDeltas.byteLength + rawDelta.byteLength);
tmp.set(tile.rawDeltas, 0);
tmp.set(rawDelta, tile.rawDeltas.byteLength);
tile.rawDeltas = tmp;
}
// apply potentially several deltas in turn.
var i = 0;
var offset = 0;
// FIXME:used clamped array ... as a 2nd parameter
var allDeltas = window.fzstd.decompress(rawDelta);
var imgData;
// May have been changed by _ensureContext garbage collection
var canvas = tile.canvas;
if (isKeyframe)
{
// Debugging paranoia: if we get this wrong bad things happen.
if (allDeltas.length < canvas.width * canvas.height * 4)
{
window.app.console.log('Unusual keyframe possibly mis-tagged, suspicious size vs. type ' +
allDeltas.length + ' vs. ' + (canvas.width * canvas.height * 4));
}
// FIXME: use zstd to de-compress directly into a Uint8ClampedArray
var len = canvas.width * canvas.height * 4;
var pixelArray = this._unpremultiply(allDeltas, len);
imgData = new ImageData(pixelArray, canvas.width, canvas.height);
if (this._debugDeltas)
window.app.console.log('Applied keyframe ' + i++ + ' of total size ' + allDeltas.length +
' at stream offset ' + offset + ' size ' + len);
offset = len;
}
while (offset < allDeltas.length)
{
if (this._debugDeltas)
window.app.console.log('Next delta at ' + offset + ' length ' + (allDeltas.length - offset));
var delta = !offset ? allDeltas : allDeltas.subarray(offset);
// Debugging paranoia: if we get this wrong bad things happen.
if (delta.length >= canvas.width * canvas.height * 4)
{
window.app.console.log('Unusual delta possibly mis-tagged, suspicious size vs. type ' +
delta.length + ' vs. ' + (canvas.width * canvas.height * 4));
}
if (!imgData) // no keyframe
imgData = tile.imgDataCache;
if (!imgData)
{
if (this._debugDeltas)
window.app.console.log('Fetch canvas contents');
imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
}
// copy old data to work from:
var oldData = new Uint8ClampedArray(imgData.data);
var len = this._applyDeltaChunk(imgData, delta, oldData, canvas.width, canvas.height);
if (this._debugDeltas)
window.app.console.log('Applied chunk ' + i++ + ' of total size ' + delta.length +
' at stream offset ' + offset + ' size ' + len);
offset += len;
}
if (imgData)
{
// hold onto the original imgData for reuse in the no keyframe case
tile.imgDataCache = imgData;
ctx.putImageData(imgData, 0, 0);
}
if (traceEvent)
traceEvent.finish();
},
_applyDeltaChunk: function(imgData, delta, oldData, width, height) {
var pixSize = width * height * 4;
if (this._debugDeltas)
window.app.console.log('Applying a delta of length ' +
delta.length + ' canvas size: ' + pixSize);
// + ' hex: ' + hex2string(delta));
var offset = 0;
// Green-tinge the old-Data ...
if (0)
{
for (var i = 0; i < pixSize; ++i)
oldData[i*4 + 1] = 128;
}
// wipe to grey.
if (0)
{
for (var i = 0; i < pixSize * 4; ++i)
imgData.data[i] = 128;
}
// Apply delta.
var stop = false;
for (var i = 0; i < delta.length && !stop;)
{
switch (delta[i])
{
case 99: // 'c': // copy row
var count = delta[i+1];
var srcRow = delta[i+2];
var destRow = delta[i+3];
if (this._debugDeltasDetail)
window.app.console.log('[' + i + ']: copy ' + count + ' row(s) ' + srcRow + ' to ' + destRow);
i+= 4;
for (var cnt = 0; cnt < count; ++cnt)
{
var src = (srcRow + cnt) * width * 4;
var dest = (destRow + cnt) * width * 4;
for (var j = 0; j < width * 4; ++j)
{
imgData.data[dest + j] = oldData[src + j];
}
}
break;
case 100: // 'd': // new run
destRow = delta[i+1];
var destCol = delta[i+2];
var span = delta[i+3];
offset = destRow * width * 4 + destCol * 4;
if (this._debugDeltasDetail)
window.app.console.log('[' + i + ']: apply new span of size ' + span +
' at pos ' + destCol + ', ' + destRow + ' into delta at byte: ' + offset);
i += 4;
span *= 4;
// copy so this is suitably aligned for a Uint32Array view
var tmpu8 = new Uint8Array(delta.subarray(i, i + span));
var pixelData = this._unpremultiply(tmpu8, tmpu8.length);
// imgData.data[offset + 1] = 256; // debug - greener start
for (var j = 0; j < span; ++j)
imgData.data[offset++] = pixelData[j];
i += span;
// imgData.data[offset - 2] = 256; // debug - blue terminator
break;
case 116: // 't': // terminate delta new one next
stop = true;
i++;
break;
default:
console.log('[' + i + ']: ERROR: Unknown delta code ' + delta[i]);
i = delta.length;
break;
}
}
return i;
},
// Update debug overlay for a tile
_showDebugForTile: function(key) {
if (!this._debug.debugOn)
return;
var tile = this._tiles[key];
tile._debugTime = this._debug.getTimeArray();
},
_queueAcknowledgement: function (tileMsgObj) {
// Queue acknowledgment, that the tile message arrived
this._queuedProcessed.push(+tileMsgObj.wireId);
},
_onTileMsg: function (textMsg, img) {
var tileMsgObj = app.socket.parseServerCmd(textMsg);
this._checkTileMsgObject(tileMsgObj);
if (this._debug.tileDataOn) {
this._debug.tileDataAddMessage();
}
// a rather different code-path with a png; should have its own msg perhaps.
if (tileMsgObj.id !== undefined) {
this._map.fire('tilepreview', {
tile: img,
id: tileMsgObj.id,
width: tileMsgObj.width,
height: tileMsgObj.height,
part: tileMsgObj.part,
mode: (tileMsgObj.mode !== undefined) ? tileMsgObj.mode : 0,
docType: this._docType
});
this._queueAcknowledgement(tileMsgObj);
return;
}
var coords = this._tileMsgToCoords(tileMsgObj);
var key = this._tileCoordsToKey(coords);
var tile = this._tiles[key];
if (!tile)
tile = this.createTile(coords, key, tileMsgObj.wireId);
tile.viewId = tileMsgObj.nviewid;
// update monotonic timestamp
tile.wireId = +tileMsgObj.wireId;
if (tile.invalidFrom == tile.wireId)
window.app.console.debug('Nasty - updated wireId matches old one');
var hasContent = img != null;
// obscure case: we could have garbage collected the
// keyframe content in JS but coolwsd still thinks we have
// it and now we just have a delta with nothing to apply
// it to; if so, mark it bad to re-fetch.
if (img && !img.isKeyframe && !tile.hasKeyframe())
{
window.app.console.debug('Unusual: Delta sent - but we have no keyframe for ' + key);
// force keyframe
tile.wireId = 0;
tile.invalidFrom = 0;
tile.gcErrors++;
// queue a later fetch of this and any other
// rogue tiles in this state
this._fetchKeyframeQueue.push(coords);
hasContent = false;
}
// updates don't need more chattiness with a tileprocessed
if (hasContent)
{
this._applyDelta(tile, img.rawData, img.isKeyframe, true);
this._tileReady(coords);
}
this._queueAcknowledgement(tileMsgObj);
},
_sendProcessedResponse: function() {
var toSend = this._queuedProcessed;
this._queuedProcessed = [];
if (toSend.length > 0)
app.socket.sendMessage('tileprocessed wids=' + toSend.join(','));
if (this._fetchKeyframeQueue.length > 0)
{
window.app.console.warn('re-fetching prematurely GCd keyframes');
this._sendTileCombineRequest(this._fetchKeyframeQueue);
this._fetchKeyframeQueue = [];
}
},
_coordsToPixBounds: function (coords) {
// coords.x and coords.y are the pixel coordinates of the top-left corner of the tile.
var topLeft = new L.Point(coords.x, coords.y);
var bottomRight = topLeft.add(new L.Point(this._tileSize, this._tileSize));
return new L.Bounds(topLeft, bottomRight);
},
updateHorizPaneSplitter: function () {
var map = this._map;
if (!this._xSplitter) {
this._xSplitter = new CSplitterLine(
map, {
name: 'horiz-pane-splitter',
fillColor: this._splittersStyleData.getPropValue('color'),
fillOpacity: this._splittersStyleData.getFloatPropValue('opacity'),
thickness: Math.round(
this._splittersStyleData.getFloatPropWithoutUnit('border-top-width')
* app.dpiScale),
isHoriz: true
});
this._canvasOverlay.initPath(this._xSplitter);
}
else {
this._xSplitter.onPositionChange();
}
},
updateVertPaneSplitter: function () {
var map = this._map;
if (!this._ySplitter) {
this._ySplitter = new CSplitterLine(
map, {
name: 'vert-pane-splitter',
fillColor: this._splittersStyleData.getPropValue('color'),
fillOpacity: this._splittersStyleData.getFloatPropValue('opacity'),
thickness: Math.round(
this._splittersStyleData.getFloatPropWithoutUnit('border-top-width')
* app.dpiScale),
isHoriz: false
});
this._canvasOverlay.initPath(this._ySplitter);
}
else {
this._ySplitter.onPositionChange();
}
},
hasXSplitter: function () {
return !!(this._xSplitter);
},
hasYSplitter: function () {
return !!(this._ySplitter);
},
getTileSectionPos: function () {
return this._painter.getTileSectionPos();
},
_coordsToTileBounds: function (coords) {
var zoomFactor = this._map.zoomToFactor(coords.z);
var tileTopLeft = new L.Point(
coords.x * this.options.tileWidthTwips / this._tileSize / zoomFactor,
coords.y * this.options.tileHeightTwips / this._tileSize / zoomFactor);
var tileSize = new L.Point(this.options.tileWidthTwips / zoomFactor, this.options.tileHeightTwips / zoomFactor);
return new L.Bounds(tileTopLeft, tileTopLeft.add(tileSize));
},
isLayoutRTL: function () {
return !!this._layoutIsRTL;
},
isCalcRTL: function () {
return this.isCalc() && this.isLayoutRTL();
}
});
L.TilesPreFetcher = L.Class.extend({
initialize: function (docLayer, map) {
this._docLayer = docLayer;
this._map = map;
},
preFetchTiles: function (forceBorderCalc, immediate) {
if (app.file.fileBasedView && this._docLayer)
this._docLayer._updateFileBasedView();
if (!this._docLayer || !this._map || this._docLayer._emptyTilesCount > 0 || !this._docLayer._canonicalIdInitialized) {
return;
}
var center = this._map.getCenter();
var zoom = this._map.getZoom();
var part = this._docLayer._selectedPart;
var mode = this._docLayer._selectedMode;
var hasEditPerm = this._map.isEditMode();
if (this._zoom === undefined) {
this._zoom = zoom;
}
if (this._preFetchPart === undefined) {
this._preFetchPart = part;
}
if (this._preFetchMode === undefined) {
this._preFetchMode = mode;
}
if (this._hasEditPerm === undefined) {
this._hasEditPerm = hasEditPerm;
}
var maxTilesToFetch = 10;
// don't search on a border wider than 5 tiles because it will freeze the UI
var maxBorderWidth = 5;
if (hasEditPerm) {
maxTilesToFetch = 5;
maxBorderWidth = 3;
}
var tileSize = this._docLayer._tileSize;
var pixelBounds = this._map.getPixelBoundsCore(center, zoom);
if (this._pixelBounds === undefined) {
this._pixelBounds = pixelBounds;
}
var splitPanesContext = this._docLayer.getSplitPanesContext();
var splitPos = splitPanesContext ? splitPanesContext.getSplitPos() : new L.Point(0, 0);
if (this._splitPos === undefined) {
this._splitPos = splitPos;
}
var paneXFixed = false;
var paneYFixed = false;
if (forceBorderCalc ||
!this._borders || this._borders.length === 0 ||
zoom !== this._zoom ||
part !== this._preFetchPart ||
mode !== this._preFetchMode ||
hasEditPerm !== this._hasEditPerm ||
!pixelBounds.equals(this._pixelBounds) ||
!splitPos.equals(this._splitPos)) {
this._zoom = zoom;
this._preFetchPart = part;
this._preFetchMode = mode;
this._hasEditPerm = hasEditPerm;
this._pixelBounds = pixelBounds;
this._splitPos = splitPos;
// Need to compute borders afresh and fetch tiles for them.
this._borders = []; // Stores borders for each split-pane.
var tileRanges = this._docLayer._pxBoundsToTileRanges(pixelBounds);
var paneStatusList = splitPanesContext ? splitPanesContext.getPanesProperties() :
[ { xFixed: false, yFixed: false} ];
window.app.console.assert(tileRanges.length === paneStatusList.length, 'tileRanges and paneStatusList should agree on the number of split-panes');
for (var paneIdx = 0; paneIdx < tileRanges.length; ++paneIdx) {
paneXFixed = paneStatusList[paneIdx].xFixed;
paneYFixed = paneStatusList[paneIdx].yFixed;
if (paneXFixed && paneYFixed) {
continue;
}
var tileRange = tileRanges[paneIdx];
var paneBorder = new L.Bounds(
tileRange.min.add(new L.Point(-1, -1)),
tileRange.max.add(new L.Point(1, 1))
);
this._borders.push(new L.TilesPreFetcher.PaneBorder(paneBorder, paneXFixed, paneYFixed));
}
}
var finalQueue = [];
var visitedTiles = {};
var validTileRange = new L.Bounds(
new L.Point(0, 0),
new L.Point(
Math.floor((this._docLayer._docWidthTwips - 1) / this._docLayer._tileWidthTwips),
Math.floor((this._docLayer._docHeightTwips - 1) / this._docLayer._tileHeightTwips)
)
);
var tilesToFetch = immediate ? Infinity : maxTilesToFetch; // total tile limit per call of preFetchTiles()
var doneAllPanes = true;
for (paneIdx = 0; paneIdx < this._borders.length; ++paneIdx) {
var queue = [];
paneBorder = this._borders[paneIdx];
var borderBounds = paneBorder.getBorderBounds();
paneXFixed = paneBorder.isXFixed();
paneYFixed = paneBorder.isYFixed();
while (tilesToFetch > 0 && paneBorder.getBorderIndex() < maxBorderWidth) {
var clampedBorder = validTileRange.clamp(borderBounds);
var fetchTopBorder = !paneYFixed && borderBounds.min.y === clampedBorder.min.y;
var fetchBottomBorder = !paneYFixed && borderBounds.max.y === clampedBorder.max.y;
var fetchLeftBorder = !paneXFixed && borderBounds.min.x === clampedBorder.min.x;
var fetchRightBorder = !paneXFixed && borderBounds.max.x === clampedBorder.max.x;
if (!fetchLeftBorder && !fetchRightBorder && !fetchTopBorder && !fetchBottomBorder) {
break;
}
if (fetchBottomBorder) {
for (var i = clampedBorder.min.x; i <= clampedBorder.max.x; i++) {
// tiles below the visible area
var coords = new L.TileCoordData(
i * tileSize,
borderBounds.max.y * tileSize);
queue.push(coords);
}
}
if (fetchTopBorder) {
for (i = clampedBorder.min.x; i <= clampedBorder.max.x; i++) {
// tiles above the visible area
coords = new L.TileCoordData(
i * tileSize,
borderBounds.min.y * tileSize);
queue.push(coords);
}
}
if (fetchRightBorder) {
for (i = clampedBorder.min.y; i <= clampedBorder.max.y; i++) {
// tiles to the right of the visible area
coords = new L.TileCoordData(
borderBounds.max.x * tileSize,
i * tileSize);
queue.push(coords);
}
}
if (fetchLeftBorder) {
for (i = clampedBorder.min.y; i <= clampedBorder.max.y; i++) {
// tiles to the left of the visible area
coords = new L.TileCoordData(
borderBounds.min.x * tileSize,
i * tileSize);
queue.push(coords);
}
}
var tilesPending = false;
for (i = 0; i < queue.length; i++) {
coords = queue[i];
coords.z = zoom;
coords.part = this._preFetchPart;
coords.mode = this._preFetchMode;
var key = this._docLayer._tileCoordsToKey(coords);
if (visitedTiles[key] ||
!this._docLayer._isValidTile(coords) ||
!this._docLayer._tileNeedsFetch(key))
continue;
if (tilesToFetch > 0) {
visitedTiles[key] = true;
finalQueue.push(coords);
tilesToFetch -= 1;
}
else {
tilesPending = true;
}
}
if (tilesPending) {
// don't update the border as there are still
// some tiles to be fetched
continue;
}
if (!paneXFixed) {
if (borderBounds.min.x > 0) {
borderBounds.min.x -= 1;
}
if (borderBounds.max.x < validTileRange.max.x) {
borderBounds.max.x += 1;
}
}
if (!paneYFixed) {
if (borderBounds.min.y > 0) {
borderBounds.min.y -= 1;
}
if (borderBounds.max.y < validTileRange.max.y) {
borderBounds.max.y += 1;
}
}
paneBorder.incBorderIndex();
} // border width loop end
if (paneBorder.getBorderIndex() < maxBorderWidth) {
doneAllPanes = false;
}
} // pane loop end
if (!immediate)
window.app.console.assert(finalQueue.length <= maxTilesToFetch,
'finalQueue length(' + finalQueue.length + ') exceeded maxTilesToFetch(' + maxTilesToFetch + ')');
var tilesRequested = false;
if (finalQueue.length > 0) {
this._cumTileCount += finalQueue.length;
this._docLayer._addTiles(finalQueue, !immediate);
tilesRequested = true;
}
if (!tilesRequested || doneAllPanes) {
this.clearTilesPreFetcher();
this._borders = undefined;
}
},
resetPreFetching: function (resetBorder) {
if (!this._map) {
return;
}
this.clearPreFetch();
if (resetBorder) {
this._borders = undefined;
}
var interval = 750;
var idleTime = 5000;
this._preFetchPart = this._docLayer._selectedPart;
this._preFetchMode = this._docLayer._selectedMode;
this._preFetchIdle = setTimeout(L.bind(function () {
this._tilesPreFetcher = setInterval(L.bind(this.preFetchTiles, this), interval);
this._preFetchIdle = undefined;
this._cumTileCount = 0;
}, this), idleTime);
},
clearPreFetch: function () {
this.clearTilesPreFetcher();
if (this._preFetchIdle !== undefined) {
clearTimeout(this._preFetchIdle);
this._preFetchIdle = undefined;
}
},
clearTilesPreFetcher: function () {
if (this._tilesPreFetcher !== undefined) {
clearInterval(this._tilesPreFetcher);
this._tilesPreFetcher = undefined;
}
},
});
L.TilesPreFetcher.PaneBorder = L.Class.extend({
initialize: function(paneBorder, paneXFixed, paneYFixed) {
this._border = paneBorder;
this._xFixed = paneXFixed;
this._yFixed = paneYFixed;
this._index = 0;
},
getBorderIndex: function () {
return this._index;
},
incBorderIndex: function () {
this._index += 1;
},
getBorderBounds: function () {
return this._border;
},
isXFixed: function () {
return this._xFixed;
},
isYFixed: function () {
return this._yFixed;
},
});
L.MessageStore = L.Class.extend({
// ownViewTypes : The types of messages related to own view.
// otherViewTypes: The types of messages related to other views.
initialize: function (ownViewTypes, otherViewTypes) {
if (!Array.isArray(ownViewTypes) || !Array.isArray(otherViewTypes)) {
window.app.console.error('Unexpected argument types');
return;
}
var ownMessages = {};
ownViewTypes.forEach(function (msgType) {
ownMessages[msgType] = '';
});
this._ownMessages = ownMessages;
var othersMessages = {};
otherViewTypes.forEach(function (msgType) {
othersMessages[msgType] = [];
});
this._othersMessages = othersMessages;
},
clear: function (notOtherMsg) {
var msgs = this._ownMessages;
Object.keys(msgs).forEach(function (msgType) {
msgs[msgType] = '';
});
if (!notOtherMsg) {
msgs = this._othersMessages;
Object.keys(msgs).forEach(function (msgType) {
msgs[msgType] = [];
});
}
},
save: function (msgType, textMsg, viewId) {
var othersMessage = (typeof viewId === 'number');
if (!othersMessage && Object.prototype.hasOwnProperty.call(this._ownMessages, msgType)) {
this._ownMessages[msgType] = textMsg;
return;
}
if (othersMessage && Object.prototype.hasOwnProperty.call(this._othersMessages, msgType)) {
this._othersMessages[msgType][viewId] = textMsg;
}
},
get: function (msgType, viewId) {
var othersMessage = (typeof viewId === 'number');
if (!othersMessage && Object.prototype.hasOwnProperty.call(this._ownMessages, msgType)) {
return this._ownMessages[msgType];
}
if (othersMessage && Object.prototype.hasOwnProperty.call(this._othersMessages, msgType)) {
return this._othersMessages[msgType][viewId];
}
},
forEach: function (callback) {
if (typeof callback !== 'function') {
window.app.console.error('Invalid callback type');
return;
}
this._cleanUpSelectionMessages(this._ownMessages);
var ownMessages = this._ownMessages;
Object.keys(this._ownMessages).forEach(function (msgType) {
callback(ownMessages[msgType]);
});
var othersMessages = this._othersMessages;
Object.keys(othersMessages).forEach(function (msgType) {
othersMessages[msgType].forEach(callback);
});
},
_cleanUpSelectionMessages: function(messages) {
// must be called only from _replayPrintTwipsMsg !!
// check if textselection is empty
// if it is, we need to handle textselectionstart and textselectionend
// otherwise we get handles without selection and they also may appear in the wrong cell
// but it is also reproducible on the same cell too. e.g. selection handles without selection
if (!messages && !messages['textselection'] && messages['textselection'] !== 'textselection: ')
return;
messages['textselectionstart'] = 'textselectionstart: ';
messages['textselectionend'] = 'textselectionend: ';
}
});