diff --git a/browser/css/toolbar.css b/browser/css/toolbar.css index 5e7910f5e251..07b3e9ddb702 100644 --- a/browser/css/toolbar.css +++ b/browser/css/toolbar.css @@ -1099,6 +1099,8 @@ button.leaflet-control-search-next .w2ui-icon.freezepanescolumn{ background: url('images/lc_freezepanescolumn.svg') no-repeat center; } .w2ui-icon.freezepanesrow{ background: url('images/lc_freezepanesrow.svg') no-repeat center; } .w2ui-icon.navigator{ background: url('images/lc_navigator.svg') no-repeat center; } +.w2ui-icon.gridvisible{ background: url('images/lc_gridvisible.svg') no-repeat center; } +.w2ui-icon.griduse{ background: url('images/lc_griduse.svg') no-repeat center; } .w2ui-icon.flipvertical { background: url('images/lc_flipvertical.svg') no-repeat center; } .w2ui-icon.fliphorizontal { background: url('images/lc_fliphorizontal.svg') no-repeat center; } diff --git a/browser/images/dark/lc_griduse.svg b/browser/images/dark/lc_griduse.svg new file mode 100644 index 000000000000..96af9867ba3f --- /dev/null +++ b/browser/images/dark/lc_griduse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/browser/images/dark/lc_gridvisible.svg b/browser/images/dark/lc_gridvisible.svg new file mode 100644 index 000000000000..75c947e1cf12 --- /dev/null +++ b/browser/images/dark/lc_gridvisible.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/browser/images/lc_griduse.svg b/browser/images/lc_griduse.svg new file mode 100644 index 000000000000..2b4176477e4e --- /dev/null +++ b/browser/images/lc_griduse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/browser/images/lc_gridvisible.svg b/browser/images/lc_gridvisible.svg new file mode 100644 index 000000000000..59367555626c --- /dev/null +++ b/browser/images/lc_gridvisible.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/browser/src/app/GraphicSelectionMiddleware.ts b/browser/src/app/GraphicSelectionMiddleware.ts index 4cb10b90c7e8..805b071dd081 100644 --- a/browser/src/app/GraphicSelectionMiddleware.ts +++ b/browser/src/app/GraphicSelectionMiddleware.ts @@ -101,13 +101,11 @@ class GraphicSelection { // When a shape is selected, the rectangles of other shapes are also sent from the core side. // They are in twips units. static convertObjectRectangleTwipsToPixels() { - const correction = 0.567; // Correction for impress case. - if (this.extraInfo && this.extraInfo.ObjectRectangles) { for (let i = 0; i < this.extraInfo.ObjectRectangles.length; i++) { for (let j = 0; j < 4; j++) this.extraInfo.ObjectRectangles[i][j] *= - app.twipsToPixels * correction; + app.twipsToPixels * app.impress.twipsCorrection; } } } diff --git a/browser/src/canvas/sections/CellCursorSection.ts b/browser/src/canvas/sections/CellCursorSection.ts index 12d9c8d3b3c1..b04c410adc97 100644 --- a/browser/src/canvas/sections/CellCursorSection.ts +++ b/browser/src/canvas/sections/CellCursorSection.ts @@ -84,7 +84,7 @@ class CellCursorSection extends CanvasSectionObject { } let x: number = 0; - if (app.isCalcRTL()) { + if (app.calc.isRTL()) { const rightMost = this.containerObject.getDocumentAnchor()[0] + this.containerObject.getDocumentAnchorSection().size[0]; x = rightMost - penX * 2 - app.calc.cellCursorRectangle.pWidth; } diff --git a/browser/src/canvas/sections/CommentListSection.ts b/browser/src/canvas/sections/CommentListSection.ts index 1965a2e5265e..3681173d4b6f 100644 --- a/browser/src/canvas/sections/CommentListSection.ts +++ b/browser/src/canvas/sections/CommentListSection.ts @@ -1601,7 +1601,7 @@ export class CommentSection extends app.definitions.canvasSectionObject { private adjustCommentFileBasedView (comment: any): void { // Below calculations are the same with the ones we do while drawing tiles in fileBasedView. var partHeightTwips = this.sectionProperties.docLayer._partHeightTwips + this.sectionProperties.docLayer._spaceBetweenParts; - var index = app.impress.partHashes.indexOf(String(comment.parthash)); + var index = app.impress.getIndexFromSlideHash(comment.parthash); var yAddition = index * partHeightTwips; comment.yAddition = yAddition; // We'll use this while we save the new position of the comment. @@ -1747,7 +1747,7 @@ export class CommentSection extends app.definitions.canvasSectionObject { if (selectedComment) { const posX = (this.sectionProperties.showSelectedBigger ? - Math.round((document.getElementById('document-container').getBoundingClientRect().width - subList[i].sectionProperties.container.getBoundingClientRect().width)/2) : + Math.round((document.getElementById('document-container').getBoundingClientRect().width - subList[i].sectionProperties.container.getBoundingClientRect().width)/2) : Math.round(actualPosition[0] / app.dpiScale) - this.sectionProperties.deflectionOfSelectedComment * (isRTL ? -1 : 1)); (new L.PosAnimation()).run(subList[i].sectionProperties.container, {x: posX, y: Math.round(lastY / app.dpiScale)}, this.getAnimationDuration()); } diff --git a/browser/src/canvas/sections/CommentSection.ts b/browser/src/canvas/sections/CommentSection.ts index 0f00f8dd38bc..b7baacbd920a 100644 --- a/browser/src/canvas/sections/CommentSection.ts +++ b/browser/src/canvas/sections/CommentSection.ts @@ -117,7 +117,7 @@ export class Comment extends CanvasSectionObject { if (this.sectionProperties.docLayer._docType === 'presentation' || this.sectionProperties.docLayer._docType === 'drawing') { this.sectionProperties.parthash = this.sectionProperties.data.parthash; - this.sectionProperties.partIndex = app.impress.partHashes.indexOf(String(this.sectionProperties.parthash)); + this.sectionProperties.partIndex = app.impress.getIndexFromSlideHash(this.sectionProperties.parthash); } this.sectionProperties.isHighlighted = false; @@ -693,7 +693,7 @@ export class Comment extends CanvasSectionObject { }), draggable: true }); - if (app.impress.partHashes[this.sectionProperties.docLayer._selectedPart] === this.sectionProperties.data.parthash || app.file.fileBasedView) + if (app.impress.partList[this.sectionProperties.docLayer._selectedPart].hash === parseInt(this.sectionProperties.data.parthash) || app.file.fileBasedView) this.map.addLayer(this.sectionProperties.annotationMarker); } if (this.sectionProperties.data.rectangle != null) { diff --git a/browser/src/canvas/sections/ShapeHandleScalingSubSection.ts b/browser/src/canvas/sections/ShapeHandleScalingSubSection.ts index 53e0f302bd2a..df6440aa3e7b 100644 --- a/browser/src/canvas/sections/ShapeHandleScalingSubSection.ts +++ b/browser/src/canvas/sections/ShapeHandleScalingSubSection.ts @@ -214,7 +214,10 @@ class ShapeHandleScalingSubSection extends CanvasSectionObject { e.stopPropagation(); this.sectionProperties.parentHandlerSection.sectionProperties.svg.style.opacity = 0.5; this.moveHandlesOnDrag(point); - this.sectionProperties.parentHandlerSection.checkObjectsBoundaries([this.position[0]], [this.position[1]]); + + // Here we are checking a point, so the size 0. dragDistance is also 0 because we already set the new position (moveHandlesOnDrag). + this.sectionProperties.parentHandlerSection.checkHelperLinesAndSnapPoints([0, 0], this.position, [0, 0]); + this.containerObject.requestReDraw(); this.sectionProperties.parentHandlerSection.showSVG(); } diff --git a/browser/src/canvas/sections/ShapeHandlesSection.ts b/browser/src/canvas/sections/ShapeHandlesSection.ts index 63a16b8d01c5..a364119cfa1c 100644 --- a/browser/src/canvas/sections/ShapeHandlesSection.ts +++ b/browser/src/canvas/sections/ShapeHandlesSection.ts @@ -751,14 +751,108 @@ class ShapeHandlesSection extends CanvasSectionObject { else this.sectionProperties.closestY = null; } + private cloneSelectedPartInfoForGridSnap() { + const selectedPart = Object.assign({}, app.impress.partList[app.map._docLayer._selectedPart]); + selectedPart.leftBorder *= app.impress.twipsCorrection; + selectedPart.upperBorder *= app.impress.twipsCorrection; + selectedPart.rightBorder *= app.impress.twipsCorrection; + selectedPart.lowerBorder *= app.impress.twipsCorrection; + selectedPart.gridCoarseWidth *= app.impress.twipsCorrection; + selectedPart.gridCoarseHeight *= app.impress.twipsCorrection; + + return selectedPart; + } + + private getInnerRecrangleForGridSnap(selectedPart: any) { + return new cool.SimpleRectangle( + selectedPart.leftBorder, + selectedPart.upperBorder, + (selectedPart.width - selectedPart.leftBorder - selectedPart.rightBorder), + (selectedPart.height - selectedPart.upperBorder - selectedPart.lowerBorder) + ); + } + + private getCornerPointsForGridSnap(size: number[], position: number[], dragDistance: number[]) { + return [ + new cool.SimplePoint((position[0] + dragDistance[0]) * app.pixelsToTwips, (position[1] + dragDistance[1]) * app.pixelsToTwips), + new cool.SimplePoint((size[0] + position[0] + dragDistance[0]) * app.pixelsToTwips, (position[1] + dragDistance[1]) * app.pixelsToTwips), + new cool.SimplePoint((position[0] + dragDistance[0]) * app.pixelsToTwips, (size[1] + position[1] + dragDistance[1]) * app.pixelsToTwips), + new cool.SimplePoint((size[0] + position[0] + dragDistance[0]) * app.pixelsToTwips, (size[1] + position[1] + dragDistance[1]) * app.pixelsToTwips), + ]; + } + + private findClosestGridPoint(size: number[], position: number[], dragDistance: number[]) { + // First rule of snap-to-grid: If you enable snap-to-grid, you have to snap. + + const selectedPart = this.cloneSelectedPartInfoForGridSnap(); + + // The 4 corners of selected object's rectangle. + const checkList = this.getCornerPointsForGridSnap(size, position, dragDistance); + + // The rectangle that is shaped by the page margins. + const innerRectangle = this.getInnerRecrangleForGridSnap(selectedPart); + + const gapX = selectedPart.gridCoarseWidth / (selectedPart.innerSpacesX > 0 ? selectedPart.innerSpacesX : 1); + const gapY = selectedPart.gridCoarseHeight / (selectedPart.innerSpacesY > 0 ? selectedPart.innerSpacesY : 1); + + let minX = 100000; + let minY = 100000; + for (let i = 0; i < 1; i++) { + if (innerRectangle.containsPoint(checkList[i].toArray())) { + + const countX = Math.round((checkList[i].x - innerRectangle.x1) / gapX); + const countY = Math.round((checkList[i].y - innerRectangle.y1) / gapY); + + const diffX = Math.abs(checkList[i].x - innerRectangle.x1 - gapX * countX); + const diffY = Math.abs(checkList[i].y - innerRectangle.y1 - gapY * countY); + + if (diffX < minX) { + minX = diffX; + this.sectionProperties.closestX = innerRectangle.x1 + (countX * gapX); + this.sectionProperties.pickedIndexX = [1, 3].includes(i) ? 0 : 1; // Do we substract width or not. + } + if (diffY < minY) { + minY = diffY; + this.sectionProperties.closestY = innerRectangle.y1 + (countY * gapY); + this.sectionProperties.pickedIndexY = [2, 3].includes(i) ? 0 : 1; // Do we substract height or not. + } + } + } + + this.sectionProperties.closestX *= app.twipsToPixels; + this.sectionProperties.closestY *= app.twipsToPixels; + } + public checkObjectsBoundaries(xListToCheck: number[], yListToCheck: number[]) { if (app.map._docLayer._docType === 'presentation') { this.findClosestX(xListToCheck); this.findClosestY(yListToCheck); - this.containerObject.requestReDraw(); } } + public checkHelperLinesAndSnapPoints(size: number[], position: number[], dragDistance: number[]) { + /* + We will first check if grid-snap is enabled and if we are close to a grid point. + If there is a grid point to snap to, then we'll ignore helper lines. + Because core side doesn't know about our helper lines, and it'll ignore them if it can snap to a grid point. + */ + + this.sectionProperties.closestX = null; + this.sectionProperties.closestY = null; + + if (app.map.stateChangeHandler.getItemValue('.uno:GridUse') === 'true') { + this.findClosestGridPoint(size, position, dragDistance) + } + else { + this.checkObjectsBoundaries( + [position[0] + dragDistance[0], position[0] + dragDistance[0] + size[0]], + [position[1] + dragDistance[1], position[1] + dragDistance[1] + size[1]] + ); + } + + this.containerObject.requestReDraw(); + } + onMouseMove(position: number[], dragDistance: number[]) { if (this.containerObject.isDraggingSomething() && this.sectionProperties.svg) { (window as any).IgnorePanning = true; @@ -767,10 +861,7 @@ class ShapeHandlesSection extends CanvasSectionObject { this.sectionProperties.svg.style.top = String((this.myTopLeft[1] + dragDistance[1]) / app.dpiScale) + 'px'; this.sectionProperties.svg.style.opacity = 0.5; this.sectionProperties.lastDragDistance = [dragDistance[0], dragDistance[1]]; - this.checkObjectsBoundaries( - [this.position[0] + dragDistance[0], this.position[0] + dragDistance[0] + this.size[0]], - [this.position[1] + dragDistance[1], this.position[1] + dragDistance[1] + this.size[1]] - ); + this.checkHelperLinesAndSnapPoints(this.size, this.position, dragDistance); this.showSVG(); } @@ -874,10 +965,15 @@ class ShapeHandlesSection extends CanvasSectionObject { this.context.restore(); } + private anythingToDraw(): boolean { + return this.sectionProperties.closestX !== null || + this.sectionProperties.closestY !== null; + } + public onDraw() { if (!this.showSection || !this.isVisible) this.hideSVG(); - else if (this.sectionProperties.closestX !== null || this.sectionProperties.closestY !== null) { + else if (this.anythingToDraw()) { this.drawGuides(); } } diff --git a/browser/src/control/Control.Menubar.js b/browser/src/control/Control.Menubar.js index b3157bbaee43..12594dad597a 100644 --- a/browser/src/control/Control.Menubar.js +++ b/browser/src/control/Control.Menubar.js @@ -441,6 +441,9 @@ L.Control.Menubar = L.Control.extend({ {name: _UNO('.uno:ZoomMinus', 'presentation'), id: 'zoomout', type: 'action'}, {name: _('Reset zoom'), id: 'zoomreset', type: 'action'}, ]).concat([ + {type: 'separator'}, + {uno: '.uno:GridVisible', name: _UNO('.uno:GridVisible')}, + {uno: '.uno:GridUse', name: _UNO('.uno:GridUse')}, {type: 'separator'}, {name: _('Toggle UI Mode'), id: 'toggleuimode', type: 'action'}, {name: _('Show Ruler'), id: 'showruler', type: 'action'}, @@ -594,6 +597,9 @@ L.Control.Menubar = L.Control.extend({ {name: _UNO('.uno:ZoomMinus', 'presentation'), id: 'zoomout', type: 'action'}, {name: _('Reset zoom'), id: 'zoomreset', type: 'action'}, ]).concat([ + {type: 'separator'}, + {uno: '.uno:GridVisible', name: _UNO('.uno:GridVisible')}, + {uno: '.uno:GridUse', name: _UNO('.uno:GridUse')}, {type: 'separator'}, {name: _('Toggle UI Mode'), id: 'toggleuimode', type: 'action'}, {name: _('Dark Mode'), id: 'toggledarktheme', type: 'action'}, @@ -1789,12 +1795,12 @@ L.Control.Menubar = L.Control.extend({ $(aItem).text(_('Use Tabbed view')); } } else if (id === 'showslide') { - if (!self._map._docLayer.isHiddenSlide(self._map.getCurrentPartNumber())) + if (!app.impress.isSlideHidden(self._map.getCurrentPartNumber())) $(aItem).hide(); else $(aItem).show(); } else if (id === 'hideslide') { - if (self._map._docLayer.isHiddenSlide(self._map.getCurrentPartNumber())) + if (app.impress.isSlideHidden(self._map.getCurrentPartNumber())) $(aItem).hide(); else $(aItem).show(); diff --git a/browser/src/control/Control.Notebookbar.js b/browser/src/control/Control.Notebookbar.js index df99f7d82858..43f87856cc04 100644 --- a/browser/src/control/Control.Notebookbar.js +++ b/browser/src/control/Control.Notebookbar.js @@ -447,12 +447,12 @@ L.Control.Notebookbar = L.Control.extend({ }, onSlideHideToggle: function() { - if (!this.map._docLayer.isHiddenSlide(this.map.getCurrentPartNumber())) + if (!app.impress.isSlideHidden(this.map.getCurrentPartNumber())) $('#showslide').hide(); else $('#showslide').show(); - if (this.map._docLayer.isHiddenSlide(this.map.getCurrentPartNumber())) + if (app.impress.isSlideHidden(this.map.getCurrentPartNumber())) $('#hideslide').hide(); else $('#hideslide').show(); diff --git a/browser/src/control/Control.NotebookbarBuilder.js b/browser/src/control/Control.NotebookbarBuilder.js index 6bd167d6f38a..03c86a83df04 100644 --- a/browser/src/control/Control.NotebookbarBuilder.js +++ b/browser/src/control/Control.NotebookbarBuilder.js @@ -121,7 +121,6 @@ L.Control.NotebookbarBuilder = L.Control.JSDialogBuilder.extend({ /*Draw Home Tab*/ this._toolitemHandlers['.uno:ObjectAlign'] = function() {}; - this._toolitemHandlers['.uno:GridVisible'] = function() {}; /*Graphic Tab*/ this._toolitemHandlers['.uno:Crop'] = function() {}; diff --git a/browser/src/control/Control.NotebookbarDraw.js b/browser/src/control/Control.NotebookbarDraw.js index 86e1be7e3e6d..5139aee33843 100644 --- a/browser/src/control/Control.NotebookbarDraw.js +++ b/browser/src/control/Control.NotebookbarDraw.js @@ -406,6 +406,26 @@ L.Control.NotebookbarDraw = L.Control.NotebookbarImpress.extend({ 'text': _('Collapse Tabs'), 'accessibility': { focusBack: true, combination: 'CU', de: null } }, + { + 'type': 'toolbox', + 'children': [ + { + 'id': 'home-grid-visible', + 'type': 'toolitem', + 'text': _UNO('.uno:GridVisible'), + 'command': '.uno:GridVisible', + 'accessibility': { focusBack: true, combination: 'GV', de: null } + }, + { + 'id': 'home-grid-use', + 'type': 'toolitem', + 'text': _UNO('.uno:GridUse'), + 'command': '.uno:GridUse', + 'accessibility': { focusBack: true, combination: 'GU', de: null } + } + ], + 'vertical': 'true' + }, { 'id':'toggledarktheme', 'type': 'bigcustomtoolitem', diff --git a/browser/src/control/Control.NotebookbarImpress.js b/browser/src/control/Control.NotebookbarImpress.js index b2f6a7ca9746..30e14e29bde0 100644 --- a/browser/src/control/Control.NotebookbarImpress.js +++ b/browser/src/control/Control.NotebookbarImpress.js @@ -468,6 +468,26 @@ L.Control.NotebookbarImpress = L.Control.NotebookbarWriter.extend({ 'command': '.uno:SlideMasterPage', 'accessibility': { focusBack: true, combination: 'MP', de: null } }, + { + 'type': 'toolbox', + 'children': [ + { + 'id': 'home-grid-visible', + 'type': 'toolitem', + 'text': _('Show Grid'), + 'command': '.uno:GridVisible', + 'accessibility': { focusBack: true, combination: 'GV', de: null } + }, + { + 'id': 'home-grid-use', + 'type': 'toolitem', + 'text': _('Snap to Grid'), + 'command': '.uno:GridUse', + 'accessibility': { focusBack: true, combination: 'GU', de: null } + } + ], + 'vertical': 'true' + }, { 'id':'toggledarktheme', 'class': 'unotoggledarktheme', diff --git a/browser/src/control/Control.PartsPreview.js b/browser/src/control/Control.PartsPreview.js index 2db1e3552b2f..66f5bf266ac6 100644 --- a/browser/src/control/Control.PartsPreview.js +++ b/browser/src/control/Control.PartsPreview.js @@ -69,14 +69,10 @@ L.Control.PartsPreview = L.Control.extend({ this._partsPreviewCont.style.whiteSpace = 'nowrap'; }, - _updateDisabled: function (e) { - var parts = e.parts; - var selectedPart = e.selectedPart; - var selectedParts = e.selectedParts; - var docType = e.docType; - if (docType === 'text' || isNaN(parts)) { - return; - } + _updateDisabled: function () { + const selectedPart = app.map._docLayer._selectedPart; + + const docType = app.map._docLayer._docType; if (docType === 'presentation' || docType === 'drawing') { if (!this._previewInitialized) @@ -103,8 +99,8 @@ L.Control.PartsPreview = L.Control.extend({ } // Create the preview parts - for (var i = 0; i < parts; i++) { - this._previewTiles.push(this._createPreview(i, e.partNames[i])); + for (var i = 0; i < app.impress.partList.length; i++) { + this._previewTiles.push(this._createPreview(i, app.impress.partList[i].hash)); } if (!app.file.fileBasedView) L.DomUtil.addClass(this._previewTiles[selectedPart], 'preview-img-currentpart'); @@ -113,18 +109,16 @@ L.Control.PartsPreview = L.Control.extend({ } else { - if (e.partNames !== undefined) { - this._syncPreviews(e); - } + this._syncPreviews(); if (!app.file.fileBasedView) { // change the border style of the selected preview. - for (var j = 0; j < parts; j++) { + for (let j = 0; j < app.impress.partList.length; j++) { L.DomUtil.removeClass(this._previewTiles[j], 'preview-img-currentpart'); L.DomUtil.removeClass(this._previewTiles[j], 'preview-img-selectedpart'); if (j === selectedPart) L.DomUtil.addClass(this._previewTiles[j], 'preview-img-currentpart'); - else if (selectedParts.indexOf(j) >= 0) + else if (app.impress.partList[j].selected) L.DomUtil.addClass(this._previewTiles[j], 'preview-img-selectedpart'); } } @@ -146,10 +140,10 @@ L.Control.PartsPreview = L.Control.extend({ addPreviewFrame = 'preview-frame-portrait'; } - for (i = 0; i < parts; i++) { + for (i = 0; i < app.impress.partList.length; i++) { L.DomUtil.removeClass(this._previewTiles[i], removePreviewImg); L.DomUtil.addClass(this._previewTiles[i], addPreviewImg); - if (this._map._docLayer._hiddenSlides.has(i)) + if (app.impress.isSlideHidden(i)) L.DomUtil.addClass(this._previewTiles[i], 'hidden-slide'); else L.DomUtil.removeClass(this._previewTiles[i], 'hidden-slide'); @@ -343,7 +337,7 @@ L.Control.PartsPreview = L.Control.extend({ }, visible: function(key, options) { var part = that._findClickedPart(options.$trigger[0].parentNode); - return that._map._docLayer._docType == 'presentation' && that._map._docLayer.isHiddenSlide(parseInt(part) - 1); + return that._map._docLayer._docType === 'presentation' && app.impress.isSlideHidden(parseInt(part) - 1); } }, hideslide: { @@ -356,7 +350,7 @@ L.Control.PartsPreview = L.Control.extend({ }, visible: function(key, options) { var part = that._findClickedPart(options.$trigger[0].parentNode); - return that._map._docLayer._docType == 'presentation' && !that._map._docLayer.isHiddenSlide(parseInt(part) - 1); + return that._map._docLayer._docType === 'presentation' && !app.impress.isSlideHidden(parseInt(part) - 1); } } } @@ -486,7 +480,7 @@ L.Control.PartsPreview = L.Control.extend({ if (this.firstSelection === undefined) this.firstSelection = this._map._docLayer._selectedPart; - //deselect all slide + //deselect all slides this._map.deselectAll(); //reselect the first origianl selection @@ -503,6 +497,7 @@ L.Control.PartsPreview = L.Control.extend({ } } } else { + this._map.deselectAll(); this._map.setPart(partId); this._map.selectPart(partId, 1, false); // And select. this.firstSelection = partId; @@ -516,27 +511,27 @@ L.Control.PartsPreview = L.Control.extend({ } }, - _syncPreviews: function (e) { + _syncPreviews: function () { var it = 0; - var parts = e.parts; - if (parts !== this._previewTiles.length) { - if (Math.abs(parts - this._previewTiles.length) === 1) { - if (parts > this._previewTiles.length) { - for (it = 0; it < parts; it++) { + + if (app.impress.partList.length !== this._previewTiles.length) { + if (Math.abs(app.impress.partList.length - this._previewTiles.length) === 1) { + if (app.impress.partList.length > this._previewTiles.length) { + for (it = 0; it < app.impress.partList.length; it++) { if (it === this._previewTiles.length) { - this._insertPreview({selectedPart: it - 1, hashCode: e.partNames[it]}); + this._insertPreview({selectedPart: it - 1, hashCode: app.impress.partList[it].hash}); break; } - if (this._previewTiles[it].hash !== e.partNames[it]) { - this._insertPreview({selectedPart: it, hashCode: e.partNames[it]}); + if (this._previewTiles[it].hash !== app.impress.partList[it].hash) { + this._insertPreview({selectedPart: it, hashCode: app.impress.partList[it].hash}); break; } } } else { for (it = 0; it < this._previewTiles.length; it++) { - if (it === e.partNames.length || - this._previewTiles[it].hash !== e.partNames[it]) { + if (it === app.impress.partList.length || + this._previewTiles[it].hash !== app.impress.partList[it].hash) { this._deletePreview({selectedPart: it}); break; } @@ -545,17 +540,17 @@ L.Control.PartsPreview = L.Control.extend({ } else { // sync all, should never happen - while (this._previewTiles.length < e.partNames.length) { + while (this._previewTiles.length < app.impress.partList.length) { this._insertPreview({selectedPart: this._previewTiles.length - 1, - hashCode: e.partNames[this._previewTiles.length]}); + hashCode: app.impress.partList[this._previewTiles.length].hash}); } - while (this._previewTiles.length > e.partNames.length) { + while (this._previewTiles.length > app.impress.partList.length) { this._deletePreview({selectedPart: this._previewTiles.length - 1}); } - for (it = 0; it < e.partNames.length; it++) { - this._previewTiles[it].hash = e.partNames[it]; + for (it = 0; it < app.impress.partList.length; it++) { + this._previewTiles[it].hash = app.impress.partList[it].hash; this._previewTiles[it].src = document.querySelector('meta[name="previewSmile"]').content; this._previewTiles[it].fetched = false; } @@ -563,9 +558,9 @@ L.Control.PartsPreview = L.Control.extend({ } else { // update hash code when user click insert slide. - for (it = 0; it < parts; it++) { - if (this._previewTiles[it].hash !== e.partNames[it]) { - this._previewTiles[it].hash = e.partNames[it]; + for (it = 0; it < app.impress.partList.length; it++) { + if (this._previewTiles[it].hash !== app.impress.partList[it].hash) { + this._previewTiles[it].hash = app.impress.partList[it].hash; this._map.getPreview(it, it, this.options.maxWidth, this.options.maxHeight, {autoUpdate: this.options.autoUpdate}); } } @@ -764,10 +759,11 @@ L.Control.PartsPreview = L.Control.extend({ _handleDragStart: function (e) { // To avoid having to add a new message to move an arbitrary part, let's select the // slide that is being dragged. - var part = this.partsPreview._findClickedPart(e.target.parentNode); + const targetNode = (e.target.id.startsWith('preview') ? e.target : e.target.parentNode); + var part = this.partsPreview._findClickedPart(targetNode); if (part !== null) { var partId = parseInt(part) - 1; // The first part is just a drop-site for reordering. - if (this.partsPreview._map._docLayer && !this.partsPreview._map._docLayer._selectedParts.indexOf(partId) >= 0) + if (this.partsPreview._map._docLayer && !app.impress.isSlideSelected(partId)) { this.partsPreview._map.setPart(partId); this.partsPreview._map.selectPart(partId, 1, false); // And select. diff --git a/browser/src/control/Control.PresentationBar.js b/browser/src/control/Control.PresentationBar.js index 132145f68197..bbb7ab0fb396 100644 --- a/browser/src/control/Control.PresentationBar.js +++ b/browser/src/control/Control.PresentationBar.js @@ -200,15 +200,14 @@ class PresentationBar { if (this.map.getDocType() !== 'presentation') return; - if (!this.map._docLayer.isHiddenSlide(this.map.getCurrentPartNumber())) + if (!app.impress.isSlideHidden(this.map.getCurrentPartNumber())) { this.showItem('showslide', false); - else - this.showItem('showslide', true); - - if (this.map._docLayer.isHiddenSlide(this.map.getCurrentPartNumber())) this.showItem('hideslide', false); - else + } + else { + this.showItem('showslide', true); this.showItem('hideslide', true); + } } } diff --git a/browser/src/control/Control.Tabs.js b/browser/src/control/Control.Tabs.js index 33e5eecde9b7..f7a0e6099cd6 100644 --- a/browser/src/control/Control.Tabs.js +++ b/browser/src/control/Control.Tabs.js @@ -40,7 +40,7 @@ L.Control.Tabs = L.Control.extend({ var map = this._map; var tableCell = document.getElementById('spreadsheet-toolbar'); this._tabsCont = L.DomUtil.create('div', 'spreadsheet-tabs-container', tableCell); - var that = this; + function areTabsMultiple() { var numItems = $('.spreadsheet-tab').length; if (numItems === 1) { @@ -77,7 +77,7 @@ L.Control.Tabs = L.Control.extend({ name: _UNO('.uno:Show', 'spreadsheet', true), callback: (this._showSheet).bind(this), visible: function() { - return that._map.hasAnyHiddenPart(); + return app.calc.isAnyPartHidden(); } }, '.uno:Hide': { @@ -163,12 +163,12 @@ L.Control.Tabs = L.Control.extend({ 'Name' : this._menuItem['.uno:Name'], } ); - if (this._map.hasAnyHiddenPart()) { + if (app.calc.isAnyPartHidden()) { Object.assign(menuItemMobile, { 'Show' : this._menuItem['.uno:Show'], }); } - if (this._map.getNumberOfVisibleParts() !== 1) { + if (app.calc.getVisiblePartCount() !== 1) { Object.assign(menuItemMobile, { 'Remove': this._menuItem['.uno:Remove'], @@ -191,7 +191,7 @@ L.Control.Tabs = L.Control.extend({ } for (var i = 0; i < parts; i++) { - if (e.hiddenParts.indexOf(i) !== -1) + if (app.calc.isPartHidden(i)) continue; // create a drop zone indicator for the sheet tab @@ -238,7 +238,7 @@ L.Control.Tabs = L.Control.extend({ }(i).bind(this)); } - if (e.protectedParts[i]) { + if (app.calc.isPartProtected(i)) { L.DomUtil.addClass(tab, 'spreadsheet-tab-protected'); } else { diff --git a/browser/src/control/Control.TopToolbar.js b/browser/src/control/Control.TopToolbar.js index 58f7a31414a0..e8068d0c82f4 100644 --- a/browser/src/control/Control.TopToolbar.js +++ b/browser/src/control/Control.TopToolbar.js @@ -177,6 +177,8 @@ class TopToolbar extends JSDialog.Toolbar { {type: 'toolitem', id: 'numberformatdecdecimals', text: _UNO('.uno:NumberFormatDecDecimals', 'spreadsheet', true), visible: false, command: '.uno:NumberFormatDecDecimals'}, {type: 'toolitem', id: 'numberformatincdecimals', text: _UNO('.uno:NumberFormatIncDecimals', 'spreadsheet', true), visible: false, command: '.uno:NumberFormatIncDecimals'}, {type: 'separator', orientation: 'vertical', id: 'break-number', visible: false}, + {type: 'button', id: 'gridvisible', img: 'gridvisible', hint: _UNO('.uno:GridVisible'), uno: 'GridVisible', hidden: true}, + {type: 'button', id: 'griduse', img: 'griduse', hint: _UNO('.uno:GridUse'), uno: 'GridUse', hidden: true}, {type: 'menubutton', id: 'inserttable:InsertTableMenu', command: 'inserttable', noLabel: true, text: _('Insert table'), visible: false, lockUno: '.uno:InsertTable'}, {type: 'menubutton', id: 'menugraphic:InsertImageMenu', noLabel: true, command: '.uno:InsertGraphic', text: _UNO('.uno:InsertGraphic', '', true), visible: false, lockUno: '.uno:InsertGraphic'}, {type: 'toolitem', id: 'insertobjectchart', text: _UNO('.uno:InsertObjectChart', '', true), command: '.uno:InsertObjectChart'}, @@ -317,6 +319,7 @@ class TopToolbar extends JSDialog.Toolbar { if (this.parentContainer) { ['resetimpress', 'breaksidebar', 'modifypage', 'leftpara', 'centerpara', 'rightpara', 'justifypara', 'breakpara', 'linespacing', + 'gridvisible', 'griduse', 'breakspacing', 'defaultbullet', 'defaultnumbering', 'breakbullet', 'inserttextbox', 'inserttable', 'insertannotation', 'backcolor', 'breaksidebar', 'modifypage', 'slidechangewindow', 'customanimation', 'masterslidespanel', 'navigator' ].forEach((id) => { @@ -328,7 +331,7 @@ class TopToolbar extends JSDialog.Toolbar { break; case 'drawing': if (this.parentContainer) { - ['leftpara', 'centerpara', 'rightpara', 'justifypara', 'breakpara', 'linespacing', + ['leftpara', 'centerpara', 'rightpara', 'justifypara', 'breakpara', 'linespacing', 'gridvisible', 'griduse', 'breakspacing', 'defaultbullet', 'defaultnumbering', 'breakbullet', 'inserttextbox', 'inserttable', 'backcolor', 'breaksidebar', 'sidebar', 'insertconnectors' ].forEach((id) => { diff --git a/browser/src/control/Parts.js b/browser/src/control/Parts.js index 46e8ddf84fca..0435c1b2284b 100644 --- a/browser/src/control/Parts.js +++ b/browser/src/control/Parts.js @@ -28,15 +28,15 @@ L.Map.include({ var docLayer = this._docLayer; var docType = docLayer._docType; - var isTheSamePart = true; + var isTheSamePart = false; // check hashes, when we add/delete/move parts they can have the same part number as before if (docType === 'spreadsheet') { isTheSamePart = app.calc.partHashes[docLayer._prevSelectedPart] === app.calc.partHashes[part]; - } else if (docType === 'presentation' || docType === 'drawing') { - isTheSamePart = - app.impress.partHashes[docLayer._prevSelectedPart] === app.impress.partHashes[part]; + } else if ((docType === 'presentation' || docType === 'drawing')) { + if (docLayer._prevSelectedPart !== undefined && part < app.impress.partList.length) + isTheSamePart = app.impress.partList[docLayer._prevSelectedPart].hash === app.impress.partList[part].hash; } else if (docType !== 'text') { console.error('Unknown docType: ' + docType); } @@ -50,7 +50,7 @@ L.Map.include({ docLayer._clearMsgReplayStore(true /* notOtherMsg*/); docLayer._prevSelectedPart = docLayer._selectedPart; - docLayer._selectedParts = []; + if (part === 'prev') { if (docLayer._selectedPart > 0) { docLayer._selectedPart -= 1; @@ -95,7 +95,6 @@ L.Map.include({ this.fire('scrolltopart'); - docLayer._selectedParts.push(docLayer._selectedPart); if (app.file.textCursor.visible) { // a click outside the slide to clear any selection app.socket.sendMessage('resetselection'); @@ -105,7 +104,6 @@ L.Map.include({ this.fire('updateparts', { selectedPart: docLayer._selectedPart, - selectedParts: docLayer._selectedParts, parts: docLayer._parts, docType: docLayer._docType }); @@ -129,46 +127,31 @@ L.Map.include({ // part is the part index/id // how is 0 to deselect, 1 to select, and 2 to toggle selection - selectPart: function (part, how, external) { - //TODO: Update/track selected parts(?). - var docLayer = this._docLayer; - var oldParts = docLayer._selectedParts.slice(); - var newParts = docLayer._selectedParts; - var index = docLayer._selectedParts.indexOf(part); - if (index >= 0 && how != 1) { - // Remove (i.e. deselect) - docLayer._selectedParts.splice(index, 1); - } - else if (how != 0) { - // Add (i.e. select) - docLayer._selectedParts.push(part); - } + // This function is Impress only. + selectPart: function (part, how, external, fireEvent = true) { + const currentSelectedCount = app.impress.getSelectedSlidesCount(); - // did we change anything? - if (oldParts.length === newParts.length && - oldParts.every((value, index) => { return value === newParts[index]; })) { - return; - } + const targetPart = app.impress.partList[part]; - this.fire('updateparts', { - selectedPart: docLayer._selectedPart, - selectedParts: docLayer._selectedParts, - parts: docLayer._parts, - docType: docLayer._docType - }); + if (how < 2) targetPart.selected = how; + else targetPart.selected = targetPart.selected === 1 ? 0 : 1; + + if (currentSelectedCount !== app.impress.getSelectedSlidesCount()) { + if (fireEvent) this.fire('updateparts', {}); - // If this wasn't triggered from the server, - // then notify the server of the change. - if (!external) { - app.socket.sendMessage('selectclientpart part=' + part + ' how=' + how); + // If this wasn't triggered from the server, + // then notify the server of the change. + if (!external) { + app.socket.sendMessage('selectclientpart part=' + part + ' how=' + how); + } } }, deselectAll: function() { - var docLayer = this._docLayer; - while (docLayer._selectedParts.length > 0) { - this.selectPart(docLayer._selectedParts[0], 0, false); + for (let i = 0; i < app.impress.partList.length; i++) { + this.selectPart(i, 0, false, false); } + this.fire('updateparts', {}); }, _processPreviewQueue: function() { @@ -434,7 +417,7 @@ L.Map.include({ return; } - if (this.getDocType() === 'spreadsheet' && docLayer._parts <= docLayer.hiddenParts() + 1) { + if (this.getDocType() === 'spreadsheet' && docLayer._parts <= app.calc.getHiddenPartCount() + 1) { return; } @@ -478,21 +461,20 @@ L.Map.include({ }, showPage: function () { - if (this.getDocType() === 'spreadsheet' && this.hasAnyHiddenPart()) { - var partNames_ = this._docLayer._partNames; - var hiddenParts_ = this._docLayer._hiddenParts; + if (this.getDocType() === 'spreadsheet' && app.calc.isAnyPartHidden()) { + var hiddenParts = app.calc.getHiddenPartNameArray(); - if (hiddenParts_.length > 0) { + if (app.calc.isAnyPartHidden()) { var container = document.createElement('div'); container.style.maxHeight = '300px'; container.style.overflowY = 'auto'; - for (var i = 0; i < hiddenParts_.length; i++) { + for (var i = 0; i < hiddenParts.length; i++) { var checkbox = document.createElement('input'); checkbox.type = 'checkbox'; - checkbox.id = 'hidden-part-checkbox-' + String(hiddenParts_[i]); + checkbox.id = 'hidden-part-checkbox-' + hiddenParts[i]; var label = document.createElement('label'); - label.htmlFor = 'hidden-part-checkbox-' + String(hiddenParts_[i]); - label.innerText = partNames_[hiddenParts_[i]]; + label.htmlFor = 'hidden-part-checkbox-' + hiddenParts[i]; + label.innerText = hiddenParts[i]; var newLine = document.createElement('br'); container.appendChild(checkbox); container.appendChild(label); @@ -504,7 +486,7 @@ L.Map.include({ var checkboxList = document.querySelectorAll('input[id^="hidden-part-checkbox"]'); for (var i = 0; i < checkboxList.length; i++) { if (checkboxList[i].checked === true) { - var partName_ = partNames_[parseInt(checkboxList[i].id.replace('hidden-part-checkbox-', ''))]; + var partName_ = checkboxList[i].id.replace('hidden-part-checkbox-', ''); var argument = {aTableName: {type: 'string', value: partName_}}; app.socket.sendMessage('uno .uno:Show ' + JSON.stringify(argument)); } @@ -512,22 +494,24 @@ L.Map.include({ }; this.uiManager.showInfoModal('show-sheets-modal', '', ' ', ' ', _('Close'), callback, true, 'show-sheets-modal-response'); - document.getElementById('show-sheets-modal').querySelectorAll('label')[0].outerHTML = container.outerHTML; + const modal = document.getElementById('show-sheets-modal'); + modal.insertBefore(container, modal.children[0]); } }, hidePage: function (tabNumber) { - if (this.getDocType() === 'spreadsheet' && this.getNumberOfVisibleParts() > 1) { + if (this.getDocType() === 'spreadsheet' && app.calc.getVisiblePartCount() > 1) { var argument = {nTabNumber: {type: 'int16', value: tabNumber}}; app.socket.sendMessage('uno .uno:Hide ' + JSON.stringify(argument)); } }, hideSlide: function() { - for (var index = 0; index < this._docLayer._selectedParts.length; index++) { - var id = this._docLayer._selectedParts[index]; - L.DomUtil.addClass(this._docLayer._preview._previewTiles[id], 'hidden-slide'); - this._docLayer._hiddenSlides.add(id); + for (let i = 0; i < app.impress.partList.length; i++) { + if (app.impress.partList[i].selected) { + app.impress.partList[i].visible = 0; + L.DomUtil.addClass(this._docLayer._preview._previewTiles[i], 'hidden-slide'); + } } app.socket.sendMessage('uno .uno:HideSlide'); @@ -535,36 +519,21 @@ L.Map.include({ }, showSlide: function() { - for (var index = 0; index < this._docLayer._selectedParts.length; index++) { - var id = this._docLayer._selectedParts[index]; - L.DomUtil.removeClass(this._docLayer._preview._previewTiles[id], 'hidden-slide'); - this._docLayer._hiddenSlides.delete(id); + for (let i = 0; i < app.impress.partList.length; i++) { + if (app.impress.partList[i].selected) { + app.impress.partList[i].visible = 1; + L.DomUtil.removeClass(this._docLayer._preview._previewTiles[i], 'hidden-slide'); + } } app.socket.sendMessage('uno .uno:ShowSlide'); this.fire('toggleslidehide'); }, - isHiddenPart: function (part) { - if (this.getDocType() !== 'spreadsheet') - return false; - return this._docLayer.isHiddenPart(part); - }, - - hasAnyHiddenPart: function () { - if (this.getDocType() !== 'spreadsheet') - return false; - return this._docLayer.hasAnyHiddenPart(); - }, - getNumberOfParts: function () { return this._docLayer._parts; }, - getNumberOfVisibleParts: function () { - return this.getNumberOfParts() - this._docLayer.hiddenParts(); - }, - getCurrentPartNumber: function () { return this._docLayer._selectedPart; }, diff --git a/browser/src/core/Socket.js b/browser/src/core/Socket.js index fb2ec54b384f..fb2b67be9bf6 100644 --- a/browser/src/core/Socket.js +++ b/browser/src/core/Socket.js @@ -636,6 +636,7 @@ app.definitions.Socket = L.Class.extend({ this._logSocket('INCOMING', textMsg); var command = this.parseServerCmd(textMsg); + if (textMsg.startsWith('coolserver ')) { // This must be the first message, unless we reconnect. var oldVersion = null; @@ -1295,7 +1296,7 @@ app.definitions.Socket = L.Class.extend({ } if (textMsg.startsWith('status:')) { - this._onStatusMsg(textMsg, command); + this._onStatusMsg(textMsg, JSON.parse(textMsg.replace('status:', '').replace('statusupdate:', ''))); return; } @@ -1814,13 +1815,6 @@ app.definitions.Socket = L.Class.extend({ command.hiddenparts.push(parseInt(item)); }); } - else if (tokens[i].startsWith('selectedparts=')) { - var selectedParts = tokens[i].substring(14).split(','); - command.selectedParts = []; - selectedParts.forEach(function (item) { - command.selectedParts.push(parseInt(item)); - }); - } else if (tokens[i].startsWith('rtlparts=')) { var rtlParts = tokens[i].substring(9).split(','); command.rtlParts = []; diff --git a/browser/src/docstate.js b/browser/src/docstate.js index a0f372cdd60e..afca6e9e4f48 100644 --- a/browser/src/docstate.js +++ b/browser/src/docstate.js @@ -31,8 +31,9 @@ window.app = { partHashes: null, // hashes used to distinguish parts (we use sheet name) }, impress: { - partHashes: null, // hashes used to distinguish parts - notesMode: false // Contrary to "NormalMultiPaneGUI" + partList: null, // Info for parts. + notesMode: false, // Opposite of "NormalMultiPaneGUI". + twipsCorrection: 0.567 // There is a constant ratio between tiletwips and impress page twips. For now, this seems safe to use. }, map: null, // Make map object a part of this. dispatcher: null, // A Dispatcher class instance is assigned to this. diff --git a/browser/src/docstatefunctions.js b/browser/src/docstatefunctions.js index 94e743fd60e1..f45cf402482e 100644 --- a/browser/src/docstatefunctions.js +++ b/browser/src/docstatefunctions.js @@ -134,10 +134,14 @@ app.isFollowingEditor = function () { return app.following.mode === 'editor'; }; -app.isCalcRTL = function () { - return ( - app.map._docLayer._rtlParts.indexOf(app.map._docLayer._selectedPart) >= 0 - ); +app.calc.isRTL = function () { + if (!app.map._docLayer || !app.map._docLayer._lastStatusJSON) return false; + + const part = + app.map._docLayer._lastStatusJSON.parts[app.map._docLayer._selectedPart]; + + if (part) return part.rtllayout !== 0; + else return false; }; app.setServerAuditFromCore = function (entries) { @@ -150,3 +154,117 @@ app.isExperimentalMode = function () { return app.socket.WSDServer.Options.indexOf('E') !== -1; return false; }; + +app.calc.isPartHidden = function (part) { + if (!app.map._docLayer || !app.map._docLayer._lastStatusJSON) return false; + + return app.map._docLayer._lastStatusJSON.parts[part].visible === 0; // ToDo: Move _lastStatusJSON into docstate.js +}; + +app.calc.isPartProtected = function (part) { + if (!app.map._docLayer || !app.map._docLayer._lastStatusJSON) return false; + + return app.map._docLayer._lastStatusJSON.parts[part].protected === 1; +}; + +app.calc.isAnyPartHidden = function () { + if (!app.map._docLayer || !app.map._docLayer._lastStatusJSON) return false; + + for (let i = 0; i < app.map._docLayer._lastStatusJSON.parts.length; i++) { + if (app.map._docLayer._lastStatusJSON.parts[i].visible === 0) return true; + } + return false; +}; + +app.calc.getHiddenPartCount = function () { + if (!app.map._docLayer || !app.map._docLayer._lastStatusJSON) return 0; + + let count = 0; + + for (let i = 0; i < app.map._docLayer._lastStatusJSON.parts.length; i++) { + if (app.map._docLayer._lastStatusJSON.parts[i].visible === 0) count++; + } + + return count; +}; + +app.calc.getVisiblePartCount = function () { + if (!app.map._docLayer || !app.map._docLayer._lastStatusJSON) return 0; + + let count = 0; + + for (let i = 0; i < app.map._docLayer._lastStatusJSON.parts.length; i++) { + if (app.map._docLayer._lastStatusJSON.parts[i].visible === 1) count++; + } + + return count; +}; + +app.calc.getHiddenPartNameArray = function () { + if (!app.map._docLayer || !app.map._docLayer._lastStatusJSON) return []; + + let array = []; + + for (let i = 0; i < app.map._docLayer._lastStatusJSON.parts.length; i++) { + let part = app.map._docLayer._lastStatusJSON.parts[i]; + if (part.visible === 0) array.push(part.name); + } + + return array; +}; + +app.impress.isSlideHidden = function (index) { + if (app.impress.partList) { + if (app.impress.partList.length > index) + return !app.impress.partList[index].visible; + else { + console.warn( + 'Index is bigger than the part count (isSlideHidden): ' + index, + ); + return true; + } + } else return false; +}; + +app.impress.areAllSlidesHidden = function () { + if (app.impress.partList) { + for (let i = 0; i < app.impress.partList.length; i++) { + if (app.impress.partList[i].visible === 1) return false; + } + return true; + } else return false; +}; + +app.impress.getSelectedSlidesCount = function () { + let count = 0; + + if (app.impress.partList) { + for (let i = 0; i < app.impress.partList.length; i++) { + if (app.impress.partList[i].selected === 1) count++; + } + } + + return count; +}; + +app.impress.getIndexFromSlideHash = function (hash) { + if (app.impress.partList) { + for (let i = 0; i < app.impress.partList.length; i++) { + if (app.impress.partList[i].hash === hash) return i; + } + + console.warn('No part with hash (getIndexFromSlideHash): ' + hash); + + return 0; + } else return 0; +}; + +app.impress.isSlideSelected = function (index) { + if ( + app.impress.partList && + index >= 0 && + index < app.impress.partList.length + ) { + return app.impress.partList[index].selected === 1; + } else return false; +}; diff --git a/browser/src/layer/tile/CalcTileLayer.js b/browser/src/layer/tile/CalcTileLayer.js index e93c8ff54cd2..0aa3c00a68f6 100644 --- a/browser/src/layer/tile/CalcTileLayer.js +++ b/browser/src/layer/tile/CalcTileLayer.js @@ -103,24 +103,6 @@ L.CalcTileLayer = L.CanvasTileLayer.extend({ this.requestCellCursor(); }, - isHiddenPart: function (part) { - if (!this._hiddenParts) - return false; - return this._hiddenParts.indexOf(part) !== -1; - }, - - hiddenParts: function () { - if (!this._hiddenParts) - return 0; - return this._hiddenParts.length; - }, - - hasAnyHiddenPart: function () { - if (!this._hiddenParts) - return false; - return this.hiddenParts() !== 0; - }, - _onUpdateParts: function (e) { if (typeof this._prevSelectedPart === 'number' && !e.source) { this.refreshViewData(undefined, false /* compatDataSrcOnly */, true /* sheetGeometryChanged */); @@ -234,7 +216,7 @@ L.CalcTileLayer = L.CanvasTileLayer.extend({ _onSetPartMsg: function (textMsg) { var part = parseInt(textMsg.match(/\d+/g)[0]); - if (!this.isHiddenPart(part)) { + if (!app.calc.isPartHidden(part)) { this.refreshViewData(undefined, true /* compatDataSrcOnly */, false /* sheetGeometryChanged */); this._replayPrintTwipsMsgAllViews('cellviewcursor'); this._replayPrintTwipsMsgAllViews('textviewselection'); @@ -368,53 +350,87 @@ L.CalcTileLayer = L.CanvasTileLayer.extend({ } }, - _handleRTLFlags: function (command) { - var rtlChanged = command.rtlParts === undefined; - rtlChanged = rtlChanged || this._rtlParts !== undefined && ( - command.rtlParts.length !== this._rtlParts.length - || this._rtlParts.some(function (part, index) { - return part !== command.rtlParts[index]; - })); - this._rtlParts = command.rtlParts || []; - if (rtlChanged) { - this._adjustCanvasSectionsForLayoutChange(); + _hasPartsCountOrNamesChanged(lastStatusJSON, statusJSON) { + if (!lastStatusJSON) + return true; + + if (lastStatusJSON.parts.length !== statusJSON.parts.length) + return true; + else { + for (let i = 0; i < statusJSON.parts.length; i++) { + if (statusJSON.parts[i].name !== lastStatusJSON.parts[i].name) + return true; + } + return false; + } + }, + + _refreshPartNames(statusJSON) { + this._partNames = []; + + for (let i = 0; i < statusJSON.parts.length; i++) { + this._partNames.push(statusJSON.parts[i].name); + } + }, + + _refreshPartHashes(statusJSON) { + app.calc.partHashes = []; + + for (let i = 0; i < statusJSON.parts.length; i++) { + app.calc.partHashes.push(statusJSON.parts[i].hash); } }, _onStatusMsg: function (textMsg) { console.log('DEBUG: onStatusMsg: ' + textMsg); - var command = app.socket.parseServerCmd(textMsg); - if (command.width && command.height && this._documentInfo !== textMsg) { + + const statusJSON = JSON.parse(textMsg.replace('status:', '').replace('statusupdate:', '')); + + if (statusJSON.width && statusJSON.height && this._documentInfo !== textMsg) { + const temp = this._lastStatusJSON ? Object.assign({}, this._lastStatusJSON): null; + this._lastStatusJSON = statusJSON; + this._documentInfo = textMsg; + var firstSelectedPart = (typeof this._selectedPart !== 'number'); - if (command.readonly === 1) - this._map.setPermission('readonly'); - this._docWidthTwips = command.width; - this._docHeightTwips = command.height; + + if (statusJSON.readonly) this._map.setPermission('readonly'); + + this._docWidthTwips = statusJSON.width; + this._docHeightTwips = statusJSON.height; + app.file.size.twips = [this._docWidthTwips, this._docHeightTwips]; app.file.size.pixels = [Math.round(this._tileSize * (this._docWidthTwips / this._tileWidthTwips)), Math.round(this._tileSize * (this._docHeightTwips / this._tileHeightTwips))]; app.view.size.pixels = app.file.size.pixels.slice(); - this._docType = command.type; - this._parts = command.parts; + + this._docType = statusJSON.type; + this._parts = statusJSON.partscount; + if (app.socket._reconnecting) { app.socket.sendMessage('setclientpart part=' + this._selectedPart); this._resetInternalState(); window.keyboard.hintOnscreenKeyboard(window.keyboard.onscreenKeyboardHint); } else { - this._selectedPart = command.selectedPart; + this._selectedPart = statusJSON.selectedpart; } - this._lastColumn = command.lastcolumn; - this._lastRow = command.lastrow; - this._selectedMode = (command.mode !== undefined) ? command.mode : 0; + + this._lastColumn = statusJSON.lastcolumn; + this._lastRow = statusJSON.lastrow; + this._selectedMode = (statusJSON.mode !== undefined) ? statusJSON.mode : 0; + if (this.sheetGeometry && this._selectedPart != this.sheetGeometry.getPart()) { // Core initiated sheet switch, need to get full sheetGeometry data for the selected sheet. this.requestSheetGeometryData(); } - this._viewId = parseInt(command.viewid); + + this._viewId = statusJSON.viewid; + console.assert(this._viewId >= 0, 'Incorrect viewId received: ' + this._viewId); + var mapSize = this._map.getSize(); var sizePx = this._twipsToPixels(new L.Point(this._docWidthTwips, this._docHeightTwips)); var width = sizePx.x; var height = sizePx.y; + if (width < mapSize.x || height < mapSize.y) { width = Math.max(width, mapSize.x); height = Math.max(height, mapSize.y); @@ -427,43 +443,30 @@ L.CalcTileLayer = L.CanvasTileLayer.extend({ else { this._updateMaxBounds(true); } - this._hiddenParts = command.hiddenparts || []; - - var pparts = []; - pparts.length = command.parts; - this._protectedParts = pparts.fill(false, 0, command.parts); - if (command.protectedParts) { - command.protectedParts.forEach(function(i) { - return this._protectedParts[i] = true; - }.bind(this)); - } - this._handleRTLFlags(command); - this._documentInfo = textMsg; - var partNames = textMsg.match(/[^\r\n]+/g); - // only get the last matches - var oldPartNames = this._partNames; - this._partNames = partNames.slice(partNames.length - this._parts); - app.calc.partHashes = this._partNames; // TODO: generate unique hash on the core side + this._adjustCanvasSectionsForLayoutChange(); + + this._refreshPartNames(statusJSON); + this._refreshPartHashes(statusJSON); + // if the number of parts, or order has changed then refresh comment positions - if (oldPartNames !== this._partNames) { + if (this._hasPartsCountOrNamesChanged(temp, statusJSON)) app.socket.sendMessage('commandvalues command=.uno:ViewAnnotationsPosition'); - } + + this._map.fire('updateparts', { selectedPart: this._selectedPart, parts: this._parts, docType: this._docType, - partNames: this._partNames, - hiddenParts: this._hiddenParts, - protectedParts: this._protectedParts, - source: 'status' + source: 'status', + partNames: this._partNames }); + this._resetPreFetching(true); - if (firstSelectedPart) { - this._switchSplitPanesContext(); - } + + if (firstSelectedPart) this._switchSplitPanesContext(); } else { - this._handleRTLFlags(command); + this._adjustCanvasSectionsForLayoutChange(); } var scrollSection = app.sectionContainer.getSectionWithName(L.CSections.Scroll.name); @@ -639,7 +642,7 @@ L.CalcTileLayer = L.CanvasTileLayer.extend({ }, _adjustCanvasSectionsForLayoutChange: function () { - var sheetIsRTL = this._rtlParts.indexOf(this._selectedPart) >= 0; + var sheetIsRTL = app.calc.isRTL(); if (sheetIsRTL && this._layoutIsRTL !== true) { console.log('debug: in LTR -> RTL canvas section adjustments'); var sectionContainer = app.sectionContainer; diff --git a/browser/src/layer/tile/ImpressTileLayer.js b/browser/src/layer/tile/ImpressTileLayer.js index 497460624c5f..6b4bedf8b6e7 100644 --- a/browser/src/layer/tile/ImpressTileLayer.js +++ b/browser/src/layer/tile/ImpressTileLayer.js @@ -12,7 +12,7 @@ * Impress tile layer is used to display a presentation document */ -/* global app $ L Set */ +/* global app $ L */ L.ImpressTileLayer = L.CanvasTileLayer.extend({ @@ -24,7 +24,7 @@ L.ImpressTileLayer = L.CanvasTileLayer.extend({ } this._preview = L.control.partsPreview(); - app.impress.partHashes = null; + if (window.mode.isMobile()) { this._addButton = L.control.mobileSlide(); L.DomUtil.addClass(L.DomUtil.get('mobile-edit-button'), 'impress'); @@ -99,7 +99,7 @@ L.ImpressTileLayer = L.CanvasTileLayer.extend({ comment.anchorPos = [docTopLeft[0], docTopLeft[1]]; comment.rectangle = [docTopLeft[0], docTopLeft[1], 566, 566]; - comment.parthash = app.impress.partHashes[this._selectedPart]; + comment.parthash = app.impress.partList[this._selectedPart].hash; var annotation = app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).add(comment); app.sectionContainer.getSectionWithName(L.CSections.CommentList.name).modify(annotation); }, @@ -253,24 +253,26 @@ L.ImpressTileLayer = L.CanvasTileLayer.extend({ _onSetPartMsg: function (textMsg) { var part = parseInt(textMsg.match(/\d+/g)[0]); if (part !== this._selectedPart) { + this._map.deselectAll(); // Deselect all first. This is a single selection. this._map.setPart(part, true); this._map.fire('setpart', {selectedPart: this._selectedPart}); } }, _onStatusMsg: function (textMsg) { - var command = app.socket.parseServerCmd(textMsg); + const statusJSON = JSON.parse(textMsg.replace('status:', '').replace('statusupdate:', '')); + // Since we have two status commands, remove them so we store and compare payloads only. textMsg = textMsg.replace('status: ', ''); textMsg = textMsg.replace('statusupdate: ', ''); - if (command.width && command.height && this._documentInfo !== textMsg) { - this._docWidthTwips = command.width; - this._docHeightTwips = command.height; - this._docType = command.type; + if (statusJSON.width && statusJSON.height && this._documentInfo !== textMsg) { + this._docWidthTwips = statusJSON.width; + this._docHeightTwips = statusJSON.height; + this._docType = statusJSON.type; if (this._docType === 'drawing') { L.DomUtil.addClass(L.DomUtil.get('presentation-controls-wrapper'), 'drawing'); } - this._parts = command.parts; + this._parts = statusJSON.partscount; this._partHeightTwips = this._docHeightTwips; this._partWidthTwips = this._docWidthTwips; @@ -284,31 +286,38 @@ L.ImpressTileLayer = L.CanvasTileLayer.extend({ app.file.size.pixels = [Math.round(this._tileSize * (this._docWidthTwips / this._tileWidthTwips)), Math.round(this._tileSize * (this._docHeightTwips / this._tileHeightTwips))]; app.view.size.pixels = app.file.size.pixels.slice(); + app.impress.partList = Object.assign([], statusJSON.parts); + this._updateMaxBounds(true); - this._documentInfo = textMsg; - this._viewId = parseInt(command.viewid); + + this._viewId = statusJSON.viewid; console.assert(this._viewId >= 0, 'Incorrect viewId received: ' + this._viewId); if (app.socket._reconnecting) { app.socket.sendMessage('setclientpart part=' + this._selectedPart); } else { - this._selectedPart = command.selectedPart; - this._selectedParts = command.selectedParts || [command.selectedPart]; + this._selectedPart = statusJSON.selectedpart; } - this._selectedMode = (command.mode !== undefined) ? command.mode : 0; + + this._selectedMode = (statusJSON.mode !== undefined) ? statusJSON.mode : (statusJSON.parts.length > 0 && statusJSON.parts[0].mode !== undefined ? statusJSON.parts[0].mode : 0); + + if (statusJSON.gridSnapEnabled === true) + app.map.stateChangeHandler.setItemValue('.uno:GridUse', 'true'); + else if (statusJSON.parts.length > 0 && statusJSON.parts[0].gridSnapEnabled === true) + app.map.stateChangeHandler.setItemValue('.uno:GridUse', 'true'); + + if (statusJSON.gridVisible === true) + app.map.stateChangeHandler.setItemValue('.uno:GridVisible', 'true'); + else if (statusJSON.parts.length > 0 && statusJSON.parts[0].gridVisible === true) + app.map.stateChangeHandler.setItemValue('.uno:GridVisible', 'true'); + this._resetPreFetching(true); - var partMatch = textMsg.match(/[^\r\n]+/g); - // only get the last matches - var newPartHashes = partMatch.slice(partMatch.length - this._parts); - var refreshAnnotation = app.impress.partHashes && (app.impress.partHashes.length !== newPartHashes.length || !app.impress.partHashes.every(function(element,i) { return element === newPartHashes[i]; })); - app.impress.partHashes = newPartHashes; - this._hiddenSlides = new Set(command.hiddenparts); - this._map.fire('updateparts', { - selectedPart: this._selectedPart, - selectedParts: this._selectedParts, - parts: this._parts, - docType: this._docType, - partNames: app.impress.partHashes - }); + + var refreshAnnotation = this._documentInfo !== textMsg; + + this._documentInfo = textMsg; + + this._map.fire('updateparts', {}); + if (refreshAnnotation) app.socket.sendMessage('commandvalues command=.uno:ViewAnnotations'); } @@ -332,18 +341,6 @@ L.ImpressTileLayer = L.CanvasTileLayer.extend({ this.lastWizardCommentHighlight.removeClass('impress-comment-highlight'); }, - isHiddenSlide: function(slideNum) { - if (!this._hiddenSlides) - return false; - return this._hiddenSlides.has(slideNum); - }, - - hiddenSlides: function () { - if (!this._hiddenSlides) - return 0; - return this._hiddenSlides.size; - }, - _invalidateAllPreviews: function () { L.CanvasTileLayer.prototype._invalidateAllPreviews.call(this); this._map.fire('invalidateparts'); diff --git a/browser/src/layer/tile/WriterTileLayer.js b/browser/src/layer/tile/WriterTileLayer.js index 2e98c305b177..a0a714436336 100644 --- a/browser/src/layer/tile/WriterTileLayer.js +++ b/browser/src/layer/tile/WriterTileLayer.js @@ -124,7 +124,7 @@ L.WriterTileLayer = L.CanvasTileLayer.extend({ _onSetPartMsg: function (textMsg) { var part = parseInt(textMsg.match(/\d+/g)[0]); - if (part !== this._selectedPart) { + if (part !== this._currentPage) { this._currentPage = part; this._map.fire('pagenumberchanged', { currentPage: part, @@ -135,7 +135,8 @@ L.WriterTileLayer = L.CanvasTileLayer.extend({ }, _onStatusMsg: function (textMsg) { - var command = app.socket.parseServerCmd(textMsg); + const statusJSON = JSON.parse(textMsg.replace('status:', '').replace('statusupdate:', '')); + if (app.socket._reconnecting) { // persist cursor position on reconnection // In writer, core always sends the cursor coordinates @@ -145,33 +146,32 @@ L.WriterTileLayer = L.CanvasTileLayer.extend({ this._postMouseEvent('buttondown', this.lastCursorPos.center[0], this.lastCursorPos.center[1], 1, 1, 0); this._postMouseEvent('buttonup', this.lastCursorPos.center[0], this.lastCursorPos.center[1], 1, 1, 0); } - if (!command.width || !command.height || this._documentInfo === textMsg) + if (!statusJSON.width || !statusJSON.height || this._documentInfo === textMsg) return; - var sizeChanged = command.width !== this._docWidthTwips || command.height !== this._docHeightTwips; + var sizeChanged = statusJSON.width !== this._docWidthTwips || statusJSON.height !== this._docHeightTwips; + + if (statusJSON.viewid !== undefined) this._viewId = statusJSON.viewid; - if (command.viewid !== undefined) { - this._viewId = parseInt(command.viewid); - } console.assert(this._viewId >= 0, 'Incorrect viewId received: ' + this._viewId); if (sizeChanged) { - this._docWidthTwips = command.width; - this._docHeightTwips = command.height; + this._docWidthTwips = statusJSON.width; + this._docHeightTwips = statusJSON.height; app.file.size.twips = [this._docWidthTwips, this._docHeightTwips]; app.file.size.pixels = [Math.round(this._tileSize * (this._docWidthTwips / this._tileWidthTwips)), Math.round(this._tileSize * (this._docHeightTwips / this._tileHeightTwips))]; app.view.size.pixels = app.file.size.pixels.slice(); - this._docType = command.type; + this._docType = statusJSON.type; this._updateMaxBounds(true); } this._documentInfo = textMsg; this._selectedPart = 0; - this._selectedMode = (command.mode !== undefined) ? command.mode : 0; + this._selectedMode = (statusJSON.mode !== undefined) ? statusJSON.mode : 0; this._parts = 1; - this._currentPage = command.selectedPart; - this._pages = command.parts; - app.file.writer.pageRectangleList = command.pageRectangleList.slice(); // Copy the array. + this._currentPage = statusJSON.selectedpart; + this._pages = statusJSON.partscount; + app.file.writer.pageRectangleList = statusJSON.pagerectangles.slice(); // Copy the array. this._map.fire('pagenumberchanged', { currentPage: this._currentPage, pages: this._pages, diff --git a/browser/src/map/handler/Map.Keyboard.js b/browser/src/map/handler/Map.Keyboard.js index e8d9e406ea28..2efece9b1d9d 100644 --- a/browser/src/map/handler/Map.Keyboard.js +++ b/browser/src/map/handler/Map.Keyboard.js @@ -420,6 +420,8 @@ L.Map.Keyboard = L.Handler.extend({ if (!deletePart) { var partToSelect = (ev.keyCode === this.keyCodes.UP || ev.keyCode === this.keyCodes.LEFT || ev.keyCode === this.keyCodes.PAGEUP) ? 'prev' : 'next'; + + this._map.deselectAll(); this._map.setPart(partToSelect); if (app.file.fileBasedView) this._map._docLayer._checkSelectedPart(); @@ -453,7 +455,7 @@ L.Map.Keyboard = L.Handler.extend({ } ev.preventDefault(); } - + }, // _handleKeyEvent - checks if the given keyboard event shall trigger diff --git a/browser/src/map/handler/Map.SlideShow.js b/browser/src/map/handler/Map.SlideShow.js index e4ab0570c2ac..f11d5c23b480 100644 --- a/browser/src/map/handler/Map.SlideShow.js +++ b/browser/src/map/handler/Map.SlideShow.js @@ -3,7 +3,7 @@ * L.Map.SlideShow is handling the slideShow action */ -/* global _ sanitizeUrl */ +/* global _ sanitizeUrl app */ L.Map.mergeOptions({ slideShow: true }); @@ -51,7 +51,7 @@ L.Map.SlideShow = L.Handler.extend({ return; } - if (this._map._docLayer.hiddenSlides() >= this._map.getNumberOfParts()) { + if (app.impress.areAllSlidesHidden()) { this._map.uiManager.showInfoModal('allslidehidden-modal', _('Empty Slide Show'), 'All slides are hidden!', '', _('OK'), function () { }, false, 'allslidehidden-modal-response'); return; diff --git a/browser/src/slideshow/SlideShowPresenter.ts b/browser/src/slideshow/SlideShowPresenter.ts index de5b81e415ee..d372affca82f 100644 --- a/browser/src/slideshow/SlideShowPresenter.ts +++ b/browser/src/slideshow/SlideShowPresenter.ts @@ -622,7 +622,7 @@ class SlideShowPresenter { return false; } - if (this._map._docLayer.hiddenSlides() >= this._map.getNumberOfParts()) { + if (app.impress.areAllSlidesHidden()) { this._notifyAllSlidesHidden(); return false; } diff --git a/browser/src/unocommands.js b/browser/src/unocommands.js index f978a1a64d81..a3823966e094 100644 --- a/browser/src/unocommands.js +++ b/browser/src/unocommands.js @@ -228,6 +228,8 @@ var unoCommandsArray = { 'FunctionDialog':{spreadsheet:{menu:_('~Function...'),},}, 'GoalSeekDialog':{spreadsheet:{menu:_('~Goal Seek...'),},}, 'GraphicDialog':{text:{menu:_('~Properties...'),},}, + 'GridUse':{global:{menu:_('Snap to Grid'),},}, + 'GridVisible':{global:{context:_('Display Grid'),menu:_('~Display Grid'),},}, 'Group':{global:{menu:_('~Group...'),},}, 'GroupOutlineMenu':{spreadsheet:{menu:_('~Group and Outline'),},}, 'GroupSparklines':{spreadsheet:{menu:_('Group Sparklines'),},}, diff --git a/cypress_test/integration_tests/desktop/calc/sheet_operation_spec.js b/cypress_test/integration_tests/desktop/calc/sheet_operation_spec.js index 54f0b2e9d8dc..229fe557c455 100644 --- a/cypress_test/integration_tests/desktop/calc/sheet_operation_spec.js +++ b/cypress_test/integration_tests/desktop/calc/sheet_operation_spec.js @@ -76,7 +76,7 @@ describe(['tagdesktop', 'tagnextcloud', 'tagproxy'], 'Sheet Operations.', functi //show sheet calcHelper.selectOptionFromContextMenu('Show Sheet'); cy.cGet('#show-sheets-modal').should('exist'); - cy.cGet('#hidden-part-checkbox-1').check(); + cy.cGet('#hidden-part-checkbox-Sheet2').check(); cy.cGet('#show-sheets-modal-response').click(); calcHelper.assertNumberofSheets(2); }); diff --git a/cypress_test/integration_tests/mobile/calc/sheet_operation_spec.js b/cypress_test/integration_tests/mobile/calc/sheet_operation_spec.js index c2eeaedcd73d..6752d368257e 100644 --- a/cypress_test/integration_tests/mobile/calc/sheet_operation_spec.js +++ b/cypress_test/integration_tests/mobile/calc/sheet_operation_spec.js @@ -91,7 +91,7 @@ describe(['tagmobile', 'tagnextcloud', 'tagproxy'], 'Sheet Operation', function calcHelper.selectOptionMobileWizard('Show Sheet'); cy.cGet('#mobile-wizard-content-modal-dialog-show-sheets-modal').should('exist'); - cy.cGet('#hidden-part-checkbox-1').check(); + cy.cGet('#hidden-part-checkbox-Sheet2').check(); cy.cGet('#show-sheets-modal-response').click(); calcHelper.assertNumberofSheets(2); diff --git a/kit/KitHelper.hpp b/kit/KitHelper.hpp index 3d9ed0d273a7..36fd16ee3d43 100644 --- a/kit/KitHelper.hpp +++ b/kit/KitHelper.hpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -41,6 +42,89 @@ namespace LOKitHelper } } + inline std::string getPartData(LibreOfficeKitDocument *loKitDocument, int part) + { + char* ptrToData = loKitDocument->pClass->getPartInfo(loKitDocument, part); + std::string result(ptrToData); + std::free(ptrToData); + return result; + } + + inline std::string MapToJSONString(std::unordered_map &map) + { + std::string resultingString = "{"; + for (std::unordered_map::iterator i = map.begin(); i != map.end(); i++) + { + resultingString += "\"" + i->first + "\": " + i->second + ","; + } + resultingString.pop_back(); + resultingString += "}"; + + return resultingString; + } + + inline int getMode(const std::string &partData) + { + Poco::JSON::Parser parser; + Poco::Dynamic::Var partJsonVar = parser.parse(partData); + const Poco::SharedPtr& partObject = partJsonVar.extract(); + + if (partObject->has("mode")) + return std::atoi(partObject->get("mode").toString().c_str()); + else + return 0; + } + + inline void fetchPartsData(LibreOfficeKitDocument *loKitDocument, std::unordered_map &resultInfo, int partsCount, int &mode) + { + /* + Except for Writer. + + Since parts should be an array, we will start an array and put parts into it. + We are building a JSON array. + */ + + std::string resultingPartsArray = "["; + + for (int i = 0; i < partsCount; ++i) + { + std::string partData = getPartData(loKitDocument, i); // Part data is sent from the core side as JSON string. + resultingPartsArray += partData + (i < partsCount - 1 ? ", ": "]"); + + if (i == 0) + mode = getMode(partData); + } + + resultInfo["parts"] = resultingPartsArray; + } + + inline void fetchWriterSpecificData(LibreOfficeKitDocument *loKitDocument, std::unordered_map &resultInfo) + { + std::string rectangles = loKitDocument->pClass->getPartPageRectangles(loKitDocument); + + rectangles = Util::replace(rectangles, ";", "], ["); + + resultInfo["pagerectangles"] = "[ [" + rectangles + "] ]"; + } + + inline void fetchCalcSpecificData(LibreOfficeKitDocument *loKitDocument, std::unordered_map &resultInfo, int part) + { + long lastColumn, lastRow; + loKitDocument->pClass->getDataArea(loKitDocument, part, &lastColumn, &lastRow); + resultInfo["lastcolumn"] = std::to_string(lastColumn); + resultInfo["lastrow"] = std::to_string(lastRow); + + char* value = loKitDocument->pClass->getCommandValues(loKitDocument, ".uno:ReadOnly"); + if (value) + { + const std::string isReadOnly = std::string(value); + std::free(value); + + bool readOnly = (isReadOnly.find("true") != std::string::npos); + resultInfo["readonly"] = readOnly ? "true": "false"; + } + } + inline std::string getDocumentTypeAsString(LibreOfficeKitDocument *loKitDocument) { assert(loKitDocument && "null loKitDocument"); @@ -50,146 +134,38 @@ namespace LOKitHelper inline std::string documentStatus(LibreOfficeKitDocument *loKitDocument) { - char *ptrValue; assert(loKitDocument && "null loKitDocument"); const auto type = static_cast(loKitDocument->pClass->getDocumentType(loKitDocument)); - const int parts = loKitDocument->pClass->getParts(loKitDocument); - const int part = loKitDocument->pClass->getPart(loKitDocument); - std::ostringstream oss; - oss << "type=" << documentTypeToString(type) - << " parts=" << parts - << " current=" << part; + std::unordered_map resultInfo; + + const int partsCount = loKitDocument->pClass->getParts(loKitDocument); + const int selectedPart = loKitDocument->pClass->getPart(loKitDocument); long width, height; loKitDocument->pClass->getDocumentSize(loKitDocument, &width, &height); - oss << " width=" << width - << " height=" << height - << " viewid=" << loKitDocument->pClass->getView(loKitDocument); + int viewId = loKitDocument->pClass->getView(loKitDocument); - if (type == LOK_DOCTYPE_SPREADSHEET) - { - long lastColumn, lastRow; - loKitDocument->pClass->getDataArea(loKitDocument, part, &lastColumn, &lastRow); - oss << " lastcolumn=" << lastColumn - << " lastrow=" << lastRow; - } + resultInfo["type"] = "\"" + documentTypeToString(type) + "\""; + resultInfo["partscount"] = std::to_string(partsCount); + resultInfo["selectedpart"] = std::to_string(selectedPart); + resultInfo["width"] = std::to_string(width); + resultInfo["height"] = std::to_string(height); + resultInfo["viewid"] = std::to_string(viewId); - if (type == LOK_DOCTYPE_SPREADSHEET || type == LOK_DOCTYPE_PRESENTATION || type == LOK_DOCTYPE_DRAWING) - { - std::ostringstream hposs; - std::ostringstream sposs; - std::ostringstream rtlposs; - std::ostringstream protectss; - std::string mode; - for (int i = 0; i < parts; ++i) - { - ptrValue = loKitDocument->pClass->getPartInfo(loKitDocument, i); - const std::string partinfo(ptrValue); - std::free(ptrValue); - for (const auto& prop : JsonUtil::jsonToMap(partinfo)) - { - const std::string& name = prop.first; - if (name == "visible") - { - if (prop.second == "0") - hposs << i << ','; - } - else if (name == "selected") - { - if (prop.second == "1") - sposs << i << ','; - } - else if (name == "rtllayout") - { - if (prop.second == "1") - rtlposs << i << ','; - } - else if (name == "protected") - { - if (prop.second == "1") - protectss << i << ','; - } - else if (name == "mode" && mode.empty()) - { - mode = prop.second; - } - } - } - - if (!mode.empty()) - oss << " mode=" << mode; - - std::string hiddenparts = hposs.str(); - if (!hiddenparts.empty()) - { - hiddenparts.pop_back(); // Remove last ',' - oss << " hiddenparts=" << hiddenparts; - } - - std::string selectedparts = sposs.str(); - if (!selectedparts.empty()) - { - selectedparts.pop_back(); // Remove last ',' - oss << " selectedparts=" << selectedparts; - } - - std::string rtlparts = rtlposs.str(); - if (!rtlparts.empty()) - { - rtlparts.pop_back(); // Remove last ',' - oss << " rtlparts=" << rtlparts; - } - - std::string protectparts = protectss.str(); - if (!protectparts.empty()) - { - protectparts.pop_back(); // Remove last ',' - oss << " protectedparts=" << protectparts; - } - - if (type == LOK_DOCTYPE_SPREADSHEET) - { - char* values = loKitDocument->pClass->getCommandValues(loKitDocument, ".uno:ReadOnly"); - if (values) - { - const std::string isReadOnly = std::string(values); - oss << " readonly=" << (isReadOnly.find("true") != std::string::npos); - std::free(values); - } - } - - for (int i = 0; i < parts; ++i) - { - oss << '\n'; - ptrValue = loKitDocument->pClass->getPartName(loKitDocument, i); - oss << ptrValue; - std::free(ptrValue); - } - - if (type == LOK_DOCTYPE_PRESENTATION || type == LOK_DOCTYPE_DRAWING) - { - for (int i = 0; i < parts; ++i) - { - oss << '\n'; - ptrValue = loKitDocument->pClass->getPartHash(loKitDocument, i); - oss << ptrValue; - std::free(ptrValue); - } - } - } + int mode = 0; - if (type == LOK_DOCTYPE_TEXT) - { - std::string rectangles = loKitDocument->pClass->getPartPageRectangles(loKitDocument); + if (type == LOK_DOCTYPE_SPREADSHEET) + fetchCalcSpecificData(loKitDocument, resultInfo, selectedPart); + else if (type == LOK_DOCTYPE_TEXT) + fetchWriterSpecificData(loKitDocument, resultInfo); - std::string::iterator end_pos = std::remove(rectangles.begin(), rectangles.end(), ' '); - rectangles.erase(end_pos, rectangles.end()); + if (type == LOK_DOCTYPE_SPREADSHEET || type == LOK_DOCTYPE_PRESENTATION || type == LOK_DOCTYPE_DRAWING) + fetchPartsData(loKitDocument, resultInfo, partsCount, mode); - oss << " pagerectangles=" << rectangles.c_str(); - } + resultInfo["mode"] = std::to_string(mode); - return oss.str(); + return MapToJSONString(resultInfo); } } diff --git a/test/TileCacheTests.cpp b/test/TileCacheTests.cpp index fdefaa8e1604..ce077b880af2 100644 --- a/test/TileCacheTests.cpp +++ b/test/TileCacheTests.cpp @@ -1236,45 +1236,21 @@ void TileCacheTests::testTileInvalidatePartImpress() void TileCacheTests::checkTiles(std::shared_ptr& socket, const std::string& docType, const std::string& testname) { - const std::string current = "current="; - const std::string height = "height="; - const std::string parts = "parts="; - const std::string type = "type="; - const std::string width = "width="; - int currentPart = -1; int totalParts = 0; int docHeight = 0; int docWidth = 0; + int docViewId = -1; // check total slides 10 sendTextFrame(socket, "status", testname); const auto response = assertResponseString(socket, "status:", testname); { - std::string line; - std::istringstream istr(response.substr(8)); - std::getline(istr, line); - - StringVector tokens(StringVector::tokenize(line, ' ')); -#if defined CPPUNIT_ASSERT_GREATEREQUAL - if (docType == "presentation") - CPPUNIT_ASSERT_GREATEREQUAL(static_cast(7), - tokens.size()); // We have an extra field. - else - CPPUNIT_ASSERT_GREATEREQUAL(static_cast(6), tokens.size()); -#else - if (docType == "presentation") - LOK_ASSERT_EQUAL(static_cast(7), tokens.size()); // We have an extra field. - else - LOK_ASSERT_EQUAL(static_cast(6), tokens.size()); -#endif + std::string text = docType; + + parseDocSize(response.substr(7), docType, currentPart, totalParts, docWidth, docHeight, + docViewId, testname); - // Expected format is something like 'type= parts= current= width= height='. - const std::string text = tokens[0].substr(type.size()); - totalParts = std::stoi(tokens[1].substr(parts.size())); - currentPart = std::stoi(tokens[2].substr(current.size())); - docWidth = std::stoi(tokens[3].substr(width.size())); - docHeight = std::stoi(tokens[4].substr(height.size())); LOK_ASSERT_EQUAL(docType, text); LOK_ASSERT_EQUAL(10, totalParts); LOK_ASSERT(currentPart > -1); diff --git a/test/UnitInsertDelete.cpp b/test/UnitInsertDelete.cpp index c29308a916cd..a470134fac65 100644 --- a/test/UnitInsertDelete.cpp +++ b/test/UnitInsertDelete.cpp @@ -27,52 +27,16 @@ namespace { -void getPartHashCodes(const std::string& testname, const std::string& response, - std::vector& parts) +std::vector getPartHashCodes(const Poco::SharedPtr status) { - std::string line; - std::istringstream istr(response); - std::getline(istr, line); - - TST_LOG("Reading parts from [" << response << "]."); - - // Expected format is something like 'type= parts= current= width= height= viewid= [hiddenparts=]'. - StringVector tokens(StringVector::tokenize(line, ' ')); -#if defined CPPUNIT_ASSERT_GREATEREQUAL - CPPUNIT_ASSERT_GREATEREQUAL(static_cast(7), tokens.size()); -#else - LOK_ASSERT_MESSAGE("Expected at least 7 tokens.", static_cast(7) <= tokens.size()); -#endif - - const std::string type = tokens[0].substr(std::string("type=").size()); - LOK_ASSERT_MESSAGE("Expected presentation or spreadsheet type to read part names/codes.", - type == "presentation" || type == "spreadsheet"); - - const int totalParts = std::stoi(tokens[1].substr(std::string("parts=").size())); - TST_LOG("Status reports " << totalParts << " parts."); - - Poco::RegularExpression endLine("[^\n\r]+"); - Poco::RegularExpression number("^-?[0-9]+$"); - Poco::RegularExpression::MatchVec matches; - int offset = 0; - - parts.clear(); - while (endLine.match(response, offset, matches) > 0) - { - LOK_ASSERT_EQUAL(1, (int)matches.size()); - const std::string str = response.substr(matches[0].offset, matches[0].length); - if (number.match(str, 0)) - { - parts.push_back(str); - } + std::vector partHashes; - offset = static_cast(matches[0].offset + matches[0].length); + for (std::size_t i = 0; i < status->getArray("parts")->size(); i++) + { + partHashes.push_back(status->getArray("parts")->getObject(i)->get("hash").toString()); } - TST_LOG("Found " << parts.size() << " part names/codes."); - - // Validate that Core is internally consistent when emitting status messages. - LOK_ASSERT_EQUAL(totalParts, (int)parts.size()); + return partHashes; } } @@ -97,7 +61,7 @@ UnitBase::TestResult UnitInsertDelete::testInsertDelete() { try { - std::vector parts; + std::vector currentPartHashes; std::string response; // Load a document @@ -115,10 +79,16 @@ UnitBase::TestResult UnitInsertDelete::testInsertDelete() helpers::sendTextFrame(socket, "status", testname); response = helpers::getResponseString(socket, "status:", testname); LOK_ASSERT_MESSAGE("did not receive a status: message as expected", !response.empty()); - getPartHashCodes(testname, response.substr(7), parts); - LOK_ASSERT_EQUAL(1, (int)parts.size()); - const std::string slide1Hash = parts[0]; + Poco::JSON::Parser parser; + Poco::Dynamic::Var statusJsonVar = parser.parse(response.substr(7)); + const Poco::SharedPtr& statusJsonObject = statusJsonVar.extract(); + + currentPartHashes = getPartHashCodes(statusJsonObject); + + LOK_ASSERT_EQUAL(static_cast(1), currentPartHashes.size()); + + const std::string slide1Hash = currentPartHashes[0]; // insert 10 slides TST_LOG("Inserting 10 slides."); @@ -128,13 +98,19 @@ UnitBase::TestResult UnitInsertDelete::testInsertDelete() response = helpers::getResponseString(socket, "status:", testname); LOK_ASSERT_MESSAGE("did not receive a status: message as expected", !response.empty()); - getPartHashCodes(testname, response.substr(7), parts); - LOK_ASSERT_EQUAL(it + 1, parts.size()); + + statusJsonVar = parser.parse(response.substr(7)); + const Poco::SharedPtr& loopStatusJsonObject = statusJsonVar.extract(); + + currentPartHashes = getPartHashCodes(loopStatusJsonObject); + + LOK_ASSERT_EQUAL(it + 1, currentPartHashes.size()); } LOK_ASSERT_MESSAGE("Hash code of slide #1 changed after inserting extra slides.", - parts[0] == slide1Hash); - const std::vector parts_after_insert(parts.begin(), parts.end()); + currentPartHashes[0] == slide1Hash); + + const std::vector parts_after_insert = currentPartHashes; // delete 10 slides TST_LOG("Deleting 10 slides."); @@ -146,12 +122,17 @@ UnitBase::TestResult UnitInsertDelete::testInsertDelete() response = helpers::getResponseString(socket, "status:", testname); LOK_ASSERT_MESSAGE("did not receive a status: message as expected", !response.empty()); - getPartHashCodes(testname, response.substr(7), parts); - LOK_ASSERT_EQUAL(11 - it, parts.size()); + + statusJsonVar = parser.parse(response.substr(7)); + const Poco::SharedPtr& loopStatusJsonObject = statusJsonVar.extract(); + + currentPartHashes = getPartHashCodes(loopStatusJsonObject); + + LOK_ASSERT_EQUAL(11 - it, currentPartHashes.size()); } LOK_ASSERT_MESSAGE("Hash code of slide #1 changed after deleting extra slides.", - parts[0] == slide1Hash); + currentPartHashes[0] == slide1Hash); // undo delete slides TST_LOG("Undoing 10 slide deletes."); @@ -161,13 +142,20 @@ UnitBase::TestResult UnitInsertDelete::testInsertDelete() response = helpers::getResponseString(socket, "status:", testname); LOK_ASSERT_MESSAGE("did not receive a status: message as expected", !response.empty()); - getPartHashCodes(testname, response.substr(7), parts); - LOK_ASSERT_EQUAL(it + 1, parts.size()); + + statusJsonVar = parser.parse(response.substr(7)); + const Poco::SharedPtr& loopStatusJsonObject = statusJsonVar.extract(); + + currentPartHashes = getPartHashCodes(loopStatusJsonObject); + + LOK_ASSERT_EQUAL(it + 1, currentPartHashes.size()); } LOK_ASSERT_MESSAGE("Hash code of slide #1 changed after undoing slide delete.", - parts[0] == slide1Hash); - const std::vector parts_after_undo(parts.begin(), parts.end()); + currentPartHashes[0] == slide1Hash); + + const std::vector parts_after_undo = currentPartHashes; + LOK_ASSERT_MESSAGE("Hash codes changed between deleting and undo.", parts_after_insert == parts_after_undo); @@ -179,20 +167,29 @@ UnitBase::TestResult UnitInsertDelete::testInsertDelete() response = helpers::getResponseString(socket, "status:", testname); LOK_ASSERT_MESSAGE("did not receive a status: message as expected", !response.empty()); - getPartHashCodes(testname, response.substr(7), parts); - LOK_ASSERT_EQUAL(11 - it, parts.size()); + + statusJsonVar = parser.parse(response.substr(7)); + const Poco::SharedPtr& loopStatusJsonObject = statusJsonVar.extract(); + + currentPartHashes = getPartHashCodes(loopStatusJsonObject); + + LOK_ASSERT_EQUAL(11 - it, currentPartHashes.size()); } LOK_ASSERT_MESSAGE("Hash code of slide #1 changed after redoing slide delete.", - parts[0] == slide1Hash); + currentPartHashes[0] == slide1Hash); // check total slides 1 TST_LOG("Expecting 1 slide."); helpers::sendTextFrame(socket, "status", testname); response = helpers::getResponseString(socket, "status:", testname); LOK_ASSERT_MESSAGE("did not receive a status: message as expected", !response.empty()); - getPartHashCodes(testname, response.substr(7), parts); - LOK_ASSERT_EQUAL(1, (int)parts.size()); + + statusJsonVar = parser.parse(response.substr(7)); + const Poco::SharedPtr& checkStatusJsonObject = statusJsonVar.extract(); + currentPartHashes = getPartHashCodes(checkStatusJsonObject); + + LOK_ASSERT_EQUAL((long unsigned int)1, currentPartHashes.size()); } catch (const Poco::Exception& exc) { diff --git a/test/UnitLoadTorture.cpp b/test/UnitLoadTorture.cpp index c782547f2d62..4abad0e3b446 100644 --- a/test/UnitLoadTorture.cpp +++ b/test/UnitLoadTorture.cpp @@ -79,7 +79,11 @@ void UnitLoadTorture::loadTorture(const std::string& name, const std::string& do const std::string status = COOLProtocol::getFirstLine(message); int viewid = -1; - LOK_ASSERT(COOLProtocol::getTokenIntegerFromMessage(status, "viewid", viewid)); + Poco::JSON::Parser parser; + Poco::Dynamic::Var statusJsonVar = parser.parse(status.substr(7)); + const Poco::SharedPtr& statusJsonObject = statusJsonVar.extract(); + if (statusJsonObject->has("viewid")) + viewid = std::atoi(statusJsonObject->get("viewid").toString().c_str()); LOK_ASSERT("Failed to create view in time " && viewid >= 0); diff --git a/test/UnitRenderingOptions.cpp b/test/UnitRenderingOptions.cpp index 5b89932b4951..08aab491c268 100644 --- a/test/UnitRenderingOptions.cpp +++ b/test/UnitRenderingOptions.cpp @@ -55,15 +55,11 @@ void UnitRenderingOptions::invokeWSDTest() helpers::sendTextFrame(socket, "status", testname); const auto status = helpers::assertResponseString(socket, "status:", testname); - // Expected format is something like 'status: type=text parts=2 current=0 width=12808 height=1142'. + Poco::JSON::Parser parser; + Poco::Dynamic::Var statusJsonVar = parser.parse(status.substr(7)); + const Poco::SharedPtr& statusJsonObject = statusJsonVar.extract(); - StringVector tokens(StringVector::tokenize(status, ' ')); - LOK_ASSERT_EQUAL(static_cast(8), tokens.size()); - - const std::string token = tokens[5]; - const std::string prefix = "height="; - LOK_ASSERT_EQUAL(static_cast(0), token.find(prefix)); - const int height = std::stoi(token.substr(prefix.size())); + const int height = std::stoi(statusJsonObject->get("height").toString()); // HideWhitespace was ignored, this was 32532, should be around 16706. LOK_ASSERT(height < 20000); } diff --git a/test/helpers.hpp b/test/helpers.hpp index 4fcfae720a88..48e3aed81d82 100644 --- a/test/helpers.hpp +++ b/test/helpers.hpp @@ -702,15 +702,16 @@ void parseDocSize(const std::string& message, const std::string& type, int& part, int& parts, int& width, int& height, int& viewid, const std::string& testname) { - StringVector tokens(StringVector::tokenize(message, ' ')); - - // Expected format is something like 'type= parts= current= width= height='. - const std::string text = tokens[0].substr(std::string("type=").size()); - parts = std::stoi(tokens[1].substr(std::string("parts=").size())); - part = std::stoi(tokens[2].substr(std::string("current=").size())); - width = std::stoi(tokens[3].substr(std::string("width=").size())); - height = std::stoi(tokens[4].substr(std::string("height=").size())); - viewid = std::stoi(tokens[5].substr(std::string("viewid=").size())); + Poco::JSON::Parser parser; + Poco::Dynamic::Var statusJsonVar = parser.parse(message); + const Poco::SharedPtr& statusJsonObject = statusJsonVar.extract(); + + const std::string text = statusJsonObject->get("type").toString(); + parts = std::stoi(statusJsonObject->get("partscount").toString()); + part = std::stoi(statusJsonObject->get("selectedpart").toString()); + width = std::stoi(statusJsonObject->get("width").toString()); + height = std::stoi(statusJsonObject->get("height").toString()); + viewid = std::stoi(statusJsonObject->get("viewid").toString()); LOK_ASSERT_EQUAL(type, text); LOK_ASSERT(parts > 0); LOK_ASSERT(part >= 0); diff --git a/tools/KitClient.cpp b/tools/KitClient.cpp index cc29950d606f..82a25794eadc 100644 --- a/tools/KitClient.cpp +++ b/tools/KitClient.cpp @@ -114,10 +114,6 @@ class LOKitClient: public Application continue; } std::cout << LOKitHelper::documentStatus(loKitDocument) << std::endl; - for (int i = 0; i < loKitDocument->pClass->getParts(loKitDocument); i++) - { - std::cout << " " << i << ": '" << loKitDocument->pClass->getPartName(loKitDocument, i) << '\'' << std::endl; - } } else if (tokens.equals(0, "tile")) { diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index 415186febe2e..97946d3a782b 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -2318,40 +2318,31 @@ bool ClientSession::handleKitToClientMessage(const std::shared_ptr& pay docBroker->forwardToChild(client_from_this(), renderThumbnailCmd); } - for(auto &token : tokens) - { - // Need to get the initial part id from status message - int part = -1; - if(getTokenInteger(tokens.getParam(token), "current", part)) - { - _clientSelectedPart = part; - } - - int mode = 0; - if(getTokenInteger(tokens.getParam(token), "mode", mode)) - _clientSelectedMode = static_cast(mode); + Poco::JSON::Parser parser; + Poco::Dynamic::Var statusJsonVar = parser.parse(firstLine.substr(7)); + const Poco::SharedPtr& statusJsonObject = statusJsonVar.extract(); - // Get document type too - std::string docType; - if(getTokenString(tokens.getParam(token), "type", docType)) - { - _isTextDocument = docType.find("text") != std::string::npos; - } + if (statusJsonObject->has("selectedpart")) + _clientSelectedPart = std::atoi(statusJsonObject->get("selectedpart").toString().c_str()); - // Store our Kit ViewId - int viewId = -1; - if(getTokenInteger(tokens.getParam(token), "viewid", viewId)) - _kitViewId = viewId; - } + if (statusJsonObject->has("mode")) + _clientSelectedMode = static_cast(std::atoi(statusJsonObject->get("mode").toString().c_str())); + if (statusJsonObject->has("type")) + _isTextDocument = statusJsonObject->get("type").toString() == "text"; + if (statusJsonObject->has("viewid")) + _kitViewId = std::atoi(statusJsonObject->get("viewid").toString().c_str()); // Forward the status response to the client. return forwardToClient(payload); } else if (tokens.equals(0, "statusupdate:")) { - uint32_t newValue; - if (tokens.getUInt32(7, "mode", newValue)) - this->_clientSelectedMode = static_cast(newValue); + Poco::JSON::Parser parser; + Poco::Dynamic::Var statusJsonVar = parser.parse(firstLine.substr(13)); + const Poco::SharedPtr& statusJsonObject = statusJsonVar.extract(); + + if (statusJsonObject->has("mode")) + _clientSelectedMode = static_cast(std::atoi(statusJsonObject->get("mode").toString().c_str())); } else if (tokens.equals(0, "commandvalues:")) {