From b93bf9f654c7a905c3eb33247e07dd7147f84a41 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 5 Dec 2022 12:25:06 +0100 Subject: [PATCH] [Editor] Don't use the editor parent which can be null. An annotation editor layer can be destroyed when it's invisible, hence some annotations can have a null parent but when printing/saving or when changing font size, color, ... of all added annotations (when selected with ctrl+a) we still need to have some parent properties especially the page dimensions, global scale factor and global rotation angle. This patch aims to remove all the references to the parent in the editor instances except in some cases where an editor should obviously have one. It fixes #15780. --- src/display/editor/annotation_editor_layer.js | 38 ++------ src/display/editor/editor.js | 81 ++++++++++++----- src/display/editor/freetext.js | 66 ++++++++------ src/display/editor/ink.js | 88 ++++++++++++------- src/display/editor/tools.js | 66 +++++++++++++- test/integration/freetext_editor_spec.js | 43 +++++++++ web/default_factory.js | 4 - web/interfaces.js | 3 - web/pdf_viewer.js | 7 +- 9 files changed, 270 insertions(+), 126 deletions(-) diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index f330514a4..6f39ddd7c 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -17,8 +17,6 @@ // eslint-disable-next-line max-len /** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ // eslint-disable-next-line max-len -/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */ -// eslint-disable-next-line max-len /** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ /** @typedef {import("../../web/interfaces").IL10n} IL10n */ @@ -33,7 +31,6 @@ import { InkEditor } from "./ink.js"; * @property {HTMLDivElement} div * @property {AnnotationEditorUIManager} uiManager * @property {boolean} enabled - * @property {AnnotationStorage} annotationStorage * @property {TextAccessibilityManager} [accessibilityManager] * @property {number} pageIndex * @property {IL10n} l10n @@ -73,7 +70,6 @@ class AnnotationEditorLayer { options.uiManager.registerEditorTypes([FreeTextEditor, InkEditor]); this.#uiManager = options.uiManager; - this.annotationStorage = options.annotationStorage; this.pageIndex = options.pageIndex; this.div = options.div; this.#accessibilityManager = options.accessibilityManager; @@ -213,7 +209,6 @@ class AnnotationEditorLayer { this.#uiManager.removeEditor(editor); this.detach(editor); - this.annotationStorage.remove(editor.id); editor.div.style.display = "none"; setTimeout(() => { // When the div is removed from DOM the focus can move on the @@ -244,7 +239,6 @@ class AnnotationEditorLayer { } this.attach(editor); - editor.pageIndex = this.pageIndex; editor.parent?.detach(editor); editor.setParent(this); if (editor.div && editor.isAttachedToDOM) { @@ -270,7 +264,7 @@ class AnnotationEditorLayer { this.moveEditorInDOM(editor); editor.onceAdded(); - this.addToAnnotationStorage(editor); + this.#uiManager.addToAnnotationStorage(editor); } moveEditorInDOM(editor) { @@ -282,16 +276,6 @@ class AnnotationEditorLayer { ); } - /** - * Add an editor in the annotation storage. - * @param {AnnotationEditor} editor - */ - addToAnnotationStorage(editor) { - if (!editor.isEmpty() && !this.annotationStorage.has(editor.id)) { - this.annotationStorage.setValue(editor.id, editor); - } - } - /** * Add or rebuild depending if it has been removed or not. * @param {AnnotationEditor} editor @@ -365,9 +349,9 @@ class AnnotationEditorLayer { deserialize(data) { switch (data.annotationType) { case AnnotationEditorType.FREETEXT: - return FreeTextEditor.deserialize(data, this); + return FreeTextEditor.deserialize(data, this, this.#uiManager); case AnnotationEditorType.INK: - return InkEditor.deserialize(data, this); + return InkEditor.deserialize(data, this, this.#uiManager); } return null; } @@ -384,6 +368,7 @@ class AnnotationEditorLayer { id, x: event.offsetX, y: event.offsetY, + uiManager: this.#uiManager, }); if (editor) { this.add(editor); @@ -520,8 +505,8 @@ class AnnotationEditorLayer { for (const editor of this.#editors.values()) { this.#accessibilityManager?.removePointerInTextLayer(editor.contentDiv); - editor.isAttachedToDOM = false; editor.setParent(null); + editor.isAttachedToDOM = false; editor.div.remove(); } this.div = null; @@ -571,14 +556,6 @@ class AnnotationEditorLayer { this.updateMode(); } - /** - * Get the scale factor from the viewport. - * @returns {number} - */ - get scaleFactor() { - return this.viewport.scale; - } - /** * Get page dimensions. * @returns {Object} dimensions. @@ -591,11 +568,6 @@ class AnnotationEditorLayer { return [width, height]; } - get viewportBaseDimensions() { - const { width, height, rotation } = this.viewport; - return rotation % 180 === 0 ? [width, height] : [height, width]; - } - /** * Set the dimensions of the main div. */ diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 4900ec989..19e43579b 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -15,12 +15,15 @@ // eslint-disable-next-line max-len /** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */ +// eslint-disable-next-line max-len +/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ import { bindEvents, ColorManager } from "./tools.js"; import { FeatureTest, shadow, unreachable } from "../../shared/util.js"; /** * @typedef {Object} AnnotationEditorParameters + * @property {AnnotationEditorUIManager} uiManager - the global manager * @property {AnnotationEditorLayer} parent - the layer containing this editor * @property {string} id - editor id * @property {number} x - x-coordinate @@ -41,6 +44,8 @@ class AnnotationEditor { #isInEditMode = false; + _uiManager = null; + #zIndex = AnnotationEditor._zIndex++; static _colorManager = new ColorManager(); @@ -61,15 +66,15 @@ class AnnotationEditor { this.pageIndex = parameters.parent.pageIndex; this.name = parameters.name; this.div = null; + this._uiManager = parameters.uiManager; - const [width, height] = this.parent.viewportBaseDimensions; + this.rotation = this.parent.viewport.rotation; + this.pageDimensions = this.parent.pageDimensions; + const [width, height] = this.parentDimensions; this.x = parameters.x / width; this.y = parameters.y / height; - this.rotation = this.parent.viewport.rotation; this.isAttachedToDOM = false; - - this._serialized = undefined; } static get _defaultLineColor() { @@ -80,9 +85,16 @@ class AnnotationEditor { ); } - setParent(parent) { - this._serialized = !parent ? this.serialize() : undefined; - this.parent = parent; + /** + * Add some commands into the CommandManager (undo/redo stuff). + * @param {Object} params + */ + addCommands(params) { + this._uiManager.addCommands(params); + } + + get currentLayer() { + return this._uiManager.currentLayer; } /** @@ -99,6 +111,14 @@ class AnnotationEditor { this.div.style.zIndex = this.#zIndex; } + setParent(parent) { + if (parent !== null) { + this.pageIndex = parent.pageIndex; + this.pageDimensions = parent.pageDimensions; + } + this.parent = parent; + } + /** * onfocus callback. */ @@ -130,7 +150,7 @@ class AnnotationEditor { event.preventDefault(); - if (!this.parent.isMultipleSelection) { + if (!this.parent?.isMultipleSelection) { this.commitOrRemove(); } } @@ -147,7 +167,11 @@ class AnnotationEditor { * Commit the data contained in this editor. */ commit() { - this.parent.addToAnnotationStorage(this); + this.addToAnnotationStorage(); + } + + addToAnnotationStorage() { + this._uiManager.addToAnnotationStorage(this); } /** @@ -170,7 +194,7 @@ class AnnotationEditor { * @param {number} ty - y-translation in screen coordinates. */ setAt(x, y, tx, ty) { - const [width, height] = this.parent.viewportBaseDimensions; + const [width, height] = this.parentDimensions; [tx, ty] = this.screenToPageTranslation(tx, ty); this.x = (x + tx) / width; @@ -186,7 +210,7 @@ class AnnotationEditor { * @param {number} y - y-translation in screen coordinates. */ translate(x, y) { - const [width, height] = this.parent.viewportBaseDimensions; + const [width, height] = this.parentDimensions; [x, y] = this.screenToPageTranslation(x, y); this.x += x / width; @@ -202,8 +226,7 @@ class AnnotationEditor { * @param {number} y */ screenToPageTranslation(x, y) { - const { rotation } = this.parent.viewport; - switch (rotation) { + switch (this.parentRotation) { case 90: return [y, -x]; case 180: @@ -215,13 +238,27 @@ class AnnotationEditor { } } + get parentScale() { + return this._uiManager.viewParameters.realScale; + } + + get parentRotation() { + return this._uiManager.viewParameters.rotation; + } + + get parentDimensions() { + const { realScale } = this._uiManager.viewParameters; + const [pageWidth, pageHeight] = this.pageDimensions; + return [pageWidth * realScale, pageHeight * realScale]; + } + /** * Set the dimensions of this editor. * @param {number} width * @param {number} height */ setDims(width, height) { - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.div.style.width = `${(100 * width) / parentWidth}%`; this.div.style.height = `${(100 * height) / parentHeight}%`; } @@ -235,7 +272,7 @@ class AnnotationEditor { return; } - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; if (!widthPercent) { style.width = `${(100 * parseFloat(width)) / parentWidth}%`; } @@ -302,10 +339,10 @@ class AnnotationEditor { } getRect(tx, ty) { - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; - const [pageWidth, pageHeight] = this.parent.pageDimensions; - const shiftX = (pageWidth * tx) / parentWidth; - const shiftY = (pageHeight * ty) / parentHeight; + const scale = this.parentScale; + const [pageWidth, pageHeight] = this.pageDimensions; + const shiftX = tx / scale; + const shiftY = ty / scale; const x = this.x * pageWidth; const y = this.y * pageHeight; const width = this.width * pageWidth; @@ -443,16 +480,18 @@ class AnnotationEditor { * * @param {Object} data * @param {AnnotationEditorLayer} parent + * @param {AnnotationEditorUIManager} uiManager * @returns {AnnotationEditor} */ - static deserialize(data, parent) { + static deserialize(data, parent, uiManager) { const editor = new this.prototype.constructor({ parent, id: parent.getNextId(), + uiManager, }); editor.rotation = data.rotation; - const [pageWidth, pageHeight] = parent.pageDimensions; + const [pageWidth, pageHeight] = editor.pageDimensions; const [x, y, width, height] = editor.getRectInCurrentCoords( data.rect, pageHeight diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 6042b9028..71b6be66c 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -153,12 +153,12 @@ class FreeTextEditor extends AnnotationEditor { #updateFontSize(fontSize) { const setFontsize = size => { this.editorDiv.style.fontSize = `calc(${size}px * var(--scale-factor))`; - this.translate(0, -(size - this.#fontSize) * this.parent.scaleFactor); + this.translate(0, -(size - this.#fontSize) * this.parentScale); this.#fontSize = size; this.#setEditorDimensions(); }; const savedFontsize = this.#fontSize; - this.parent.addCommands({ + this.addCommands({ cmd: () => { setFontsize(fontSize); }, @@ -178,14 +178,12 @@ class FreeTextEditor extends AnnotationEditor { */ #updateColor(color) { const savedColor = this.#color; - this.parent.addCommands({ + this.addCommands({ cmd: () => { - this.#color = color; - this.editorDiv.style.color = color; + this.#color = this.editorDiv.style.color = color; }, undo: () => { - this.#color = savedColor; - this.editorDiv.style.color = savedColor; + this.#color = this.editorDiv.style.color = savedColor; }, mustExec: true, type: AnnotationEditorParamsType.FREETEXT_COLOR, @@ -197,10 +195,10 @@ class FreeTextEditor extends AnnotationEditor { /** @inheritdoc */ getInitialTranslation() { // The start of the base line is where the user clicked. + const scale = this.parentScale; return [ - -FreeTextEditor._internalPadding * this.parent.scaleFactor, - -(FreeTextEditor._internalPadding + this.#fontSize) * - this.parent.scaleFactor, + -FreeTextEditor._internalPadding * scale, + -(FreeTextEditor._internalPadding + this.#fontSize) * scale, ]; } @@ -254,9 +252,11 @@ class FreeTextEditor extends AnnotationEditor { this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur); this.editorDiv.removeEventListener("input", this.#boundEditorDivInput); - // On Chrome, the focus is given to when contentEditable is set to - // false, hence we focus the div. - this.div.focus(); + if (this.pageIndex === this._uiManager.currentPageIndex) { + // On Chrome, the focus is given to when contentEditable is set to + // false, hence we focus the div. + this.div.focus(); + } // In case the blur callback hasn't been called. this.isEditing = false; @@ -311,8 +311,22 @@ class FreeTextEditor extends AnnotationEditor { } #setEditorDimensions() { - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; - const rect = this.div.getBoundingClientRect(); + const [parentWidth, parentHeight] = this.parentDimensions; + + let rect; + if (this.isAttachedToDOM) { + rect = this.div.getBoundingClientRect(); + } else { + // This editor isn't on screen but we need to get its dimensions, so + // we just insert it in the DOM, get its bounding box and then remove it. + const { currentLayer, div } = this; + const savedDisplay = div.style.display; + div.style.display = "hidden"; + currentLayer.div.append(this.div); + rect = div.getBoundingClientRect(); + div.remove(); + div.style.display = savedDisplay; + } this.width = rect.width / parentWidth; this.height = rect.height / parentHeight; @@ -323,6 +337,10 @@ class FreeTextEditor extends AnnotationEditor { * @returns {undefined} */ commit() { + if (!this.isInEditMode()) { + return; + } + super.commit(); if (!this.#hasAlreadyBeenCommitted) { // This editor has something and it's the first time @@ -435,7 +453,7 @@ class FreeTextEditor extends AnnotationEditor { if (this.width) { // This editor was created in using copy (ctrl+c). - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.setAt( baseX * parentWidth, baseY * parentHeight, @@ -466,8 +484,8 @@ class FreeTextEditor extends AnnotationEditor { } /** @inheritdoc */ - static deserialize(data, parent) { - const editor = super.deserialize(data, parent); + static deserialize(data, parent, uiManager) { + const editor = super.deserialize(data, parent, uiManager); editor.#fontSize = data.fontSize; editor.#color = Util.makeHexColor(...data.color); @@ -478,19 +496,17 @@ class FreeTextEditor extends AnnotationEditor { /** @inheritdoc */ serialize() { - if (this._serialized !== undefined) { - return this._serialized; - } - if (this.isEmpty()) { return null; } - const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor; + const padding = FreeTextEditor._internalPadding * this.parentScale; const rect = this.getRect(padding, padding); const color = AnnotationEditor._colorManager.convert( - getComputedStyle(this.editorDiv).color + this.isAttachedToDOM + ? getComputedStyle(this.editorDiv).color + : this.#color ); return { @@ -498,7 +514,7 @@ class FreeTextEditor extends AnnotationEditor { color, fontSize: this.#fontSize, value: this.#content, - pageIndex: this.parent.pageIndex, + pageIndex: this.pageIndex, rect, rotation: this.rotation, }; diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 434d49e13..429c44362 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -165,7 +165,7 @@ class InkEditor extends AnnotationEditor { */ #updateThickness(thickness) { const savedThickness = this.thickness; - this.parent.addCommands({ + this.addCommands({ cmd: () => { this.thickness = thickness; this.#fitToContent(); @@ -187,7 +187,7 @@ class InkEditor extends AnnotationEditor { */ #updateColor(color) { const savedColor = this.color; - this.parent.addCommands({ + this.addCommands({ cmd: () => { this.color = color; this.#redraw(); @@ -210,7 +210,7 @@ class InkEditor extends AnnotationEditor { #updateOpacity(opacity) { opacity /= 100; const savedOpacity = this.opacity; - this.parent.addCommands({ + this.addCommands({ cmd: () => { this.opacity = opacity; this.#redraw(); @@ -268,6 +268,27 @@ class InkEditor extends AnnotationEditor { super.remove(); } + setParent(parent) { + if (!this.parent && parent) { + // We've a parent hence the rescale will be handled thanks to the + // ResizeObserver. + this._uiManager.removeShouldRescale(this); + } else if (this.parent && parent === null) { + // The editor is removed from the DOM, hence we handle the rescale thanks + // to the onScaleChanging callback. + // This way, it'll be saved/printed correctly. + this._uiManager.addShouldRescale(this); + } + super.setParent(parent); + } + + onScaleChanging() { + const [parentWidth, parentHeight] = this.parentDimensions; + const width = this.width * parentWidth; + const height = this.height * parentHeight; + this.setDimensions(width, height); + } + /** @inheritdoc */ enableEditMode() { if (this.#disableEditing || this.canvas === null) { @@ -311,14 +332,17 @@ class InkEditor extends AnnotationEditor { } #getInitialBBox() { - const { width, height, rotation } = this.parent.viewport; - switch (rotation) { + const { + parentRotation, + parentDimensions: [width, height], + } = this; + switch (parentRotation) { case 90: - return [0, width, width, height]; + return [0, height, height, width]; case 180: return [width, height, width, height]; case 270: - return [height, 0, width, height]; + return [width, 0, height, width]; default: return [0, 0, width, height]; } @@ -328,12 +352,12 @@ class InkEditor extends AnnotationEditor { * Set line styles. */ #setStroke() { - this.ctx.lineWidth = - (this.thickness * this.parent.scaleFactor) / this.scaleFactor; - this.ctx.lineCap = "round"; - this.ctx.lineJoin = "round"; - this.ctx.miterLimit = 10; - this.ctx.strokeStyle = `${this.color}${opacityToHex(this.opacity)}`; + const { ctx, color, opacity, thickness, parentScale, scaleFactor } = this; + ctx.lineWidth = (thickness * parentScale) / scaleFactor; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.miterLimit = 10; + ctx.strokeStyle = `${color}${opacityToHex(opacity)}`; } /** @@ -445,7 +469,7 @@ class InkEditor extends AnnotationEditor { } }; - this.parent.addCommands({ cmd, undo, mustExec: true }); + this.addCommands({ cmd, undo, mustExec: true }); } /** @@ -493,9 +517,11 @@ class InkEditor extends AnnotationEditor { // When commiting, the position of this editor is changed, hence we must // move it to the right position in the DOM. this.parent.moveEditorInDOM(this); - // After the div has been moved in the DOM, the focus may have been stolen - // by document.body, hence we just keep it here. - this.div.focus(); + if (this.pageIndex === this._uiManager.currentPageIndex) { + // After the div has been moved in the DOM, the focus may have been stolen + // by document.body, hence we just keep it here. + this.div.focus(); + } } /** @inheritdoc */ @@ -581,7 +607,7 @@ class InkEditor extends AnnotationEditor { this.#boundCanvasPointermove ); - this.parent.addToAnnotationStorage(this); + this.addToAnnotationStorage(); } /** @@ -649,7 +675,7 @@ class InkEditor extends AnnotationEditor { if (this.width) { // This editor was created in using copy (ctrl+c). - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.setAt( baseX * parentWidth, baseY * parentHeight, @@ -676,7 +702,7 @@ class InkEditor extends AnnotationEditor { if (!this.#isCanvasInitialized) { return; } - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.canvas.width = Math.ceil(this.width * parentWidth); this.canvas.height = Math.ceil(this.height * parentHeight); this.#updateTransform(); @@ -712,7 +738,7 @@ class InkEditor extends AnnotationEditor { this.setDims(width, height); } - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.width = width / parentWidth; this.height = height / parentHeight; @@ -940,7 +966,7 @@ class InkEditor extends AnnotationEditor { */ #getPadding() { return this.#disableEditing - ? Math.ceil(this.thickness * this.parent.scaleFactor) + ? Math.ceil(this.thickness * this.parentScale) : 0; } @@ -967,7 +993,7 @@ class InkEditor extends AnnotationEditor { const width = Math.ceil(padding + this.#baseWidth * this.scaleFactor); const height = Math.ceil(padding + this.#baseHeight * this.scaleFactor); - const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; + const [parentWidth, parentHeight] = this.parentDimensions; this.width = width / parentWidth; this.height = height / parentHeight; @@ -1005,17 +1031,17 @@ class InkEditor extends AnnotationEditor { } /** @inheritdoc */ - static deserialize(data, parent) { - const editor = super.deserialize(data, parent); + static deserialize(data, parent, uiManager) { + const editor = super.deserialize(data, parent, uiManager); editor.thickness = data.thickness; editor.color = Util.makeHexColor(...data.color); editor.opacity = data.opacity; - const [pageWidth, pageHeight] = parent.pageDimensions; + const [pageWidth, pageHeight] = editor.pageDimensions; const width = editor.width * pageWidth; const height = editor.height * pageHeight; - const scaleFactor = parent.scaleFactor; + const scaleFactor = editor.parentScale; const padding = data.thickness / 2; editor.#aspectRatio = width / height; @@ -1058,10 +1084,6 @@ class InkEditor extends AnnotationEditor { /** @inheritdoc */ serialize() { - if (this._serialized !== undefined) { - return this._serialized; - } - if (this.isEmpty()) { return null; } @@ -1078,12 +1100,12 @@ class InkEditor extends AnnotationEditor { thickness: this.thickness, opacity: this.opacity, paths: this.#serializePaths( - this.scaleFactor / this.parent.scaleFactor, + this.scaleFactor / this.parentScale, this.translationX, this.translationY, height ), - pageIndex: this.parent.pageIndex, + pageIndex: this.pageIndex, rect, rotation: this.rotation, }; diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index e502934b7..80e1c4f9d 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -25,7 +25,7 @@ import { Util, warn, } from "../../shared/util.js"; -import { getColorValues, getRGB } from "../display_utils.js"; +import { getColorValues, getRGB, PixelsPerInch } from "../display_utils.js"; function bindEvents(obj, element, names) { for (const name of names) { @@ -350,12 +350,16 @@ class AnnotationEditorUIManager { #allLayers = new Map(); + #annotationStorage = null; + #commandManager = new CommandManager(); #currentPageIndex = 0; #editorTypes = null; + #editorsToRescale = new Set(); + #eventBus = null; #idManager = new IdManager(); @@ -378,6 +382,10 @@ class AnnotationEditorUIManager { #boundOnPageChanging = this.onPageChanging.bind(this); + #boundOnScaleChanging = this.onScaleChanging.bind(this); + + #boundOnRotationChanging = this.onRotationChanging.bind(this); + #previousStates = { isEditing: false, isEmpty: true, @@ -413,22 +421,32 @@ class AnnotationEditorUIManager { [["Escape", "mac+Escape"], AnnotationEditorUIManager.prototype.unselectAll], ]); - constructor(container, eventBus) { + constructor(container, eventBus, annotationStorage) { this.#container = container; this.#eventBus = eventBus; this.#eventBus._on("editingaction", this.#boundOnEditingAction); this.#eventBus._on("pagechanging", this.#boundOnPageChanging); + this.#eventBus._on("scalechanging", this.#boundOnScaleChanging); + this.#eventBus._on("rotationchanging", this.#boundOnRotationChanging); + this.#annotationStorage = annotationStorage; + this.viewParameters = { + realScale: PixelsPerInch.PDF_TO_CSS_UNITS, + rotation: 0, + }; } destroy() { this.#removeKeyboardManager(); this.#eventBus._off("editingaction", this.#boundOnEditingAction); this.#eventBus._off("pagechanging", this.#boundOnPageChanging); + this.#eventBus._off("scalechanging", this.#boundOnScaleChanging); + this.#eventBus._off("rotationchanging", this.#boundOnRotationChanging); for (const layer of this.#allLayers.values()) { layer.destroy(); } this.#allLayers.clear(); this.#allEditors.clear(); + this.#editorsToRescale.clear(); this.#activeEditor = null; this.#selectedEditors.clear(); this.#commandManager.destroy(); @@ -442,6 +460,41 @@ class AnnotationEditorUIManager { this.#container.focus(); } + addShouldRescale(editor) { + this.#editorsToRescale.add(editor); + } + + removeShouldRescale(editor) { + this.#editorsToRescale.delete(editor); + } + + onScaleChanging({ scale }) { + this.commitOrRemove(); + this.viewParameters.realScale = scale * PixelsPerInch.PDF_TO_CSS_UNITS; + for (const editor of this.#editorsToRescale) { + editor.onScaleChanging(); + } + } + + onRotationChanging({ pagesRotation }) { + this.commitOrRemove(); + this.viewParameters.rotation = pagesRotation; + } + + /** + * Add an editor in the annotation storage. + * @param {AnnotationEditor} editor + */ + addToAnnotationStorage(editor) { + if ( + !editor.isEmpty() && + this.#annotationStorage && + !this.#annotationStorage.has(editor.id) + ) { + this.#annotationStorage.setValue(editor.id, editor); + } + } + #addKeyboardManager() { // The keyboard events are caught at the container level in order to be able // to execute some callbacks even if the current page doesn't have focus. @@ -646,6 +699,14 @@ class AnnotationEditorUIManager { return this.#idManager.getId(); } + get currentLayer() { + return this.#allLayers.get(this.#currentPageIndex); + } + + get currentPageIndex() { + return this.#currentPageIndex; + } + /** * Add a new layer for a page which will contains the editors. * @param {AnnotationEditorLayer} layer @@ -783,6 +844,7 @@ class AnnotationEditorUIManager { removeEditor(editor) { this.#allEditors.delete(editor.id); this.unselect(editor); + this.#annotationStorage?.remove(editor.id); } /** diff --git a/test/integration/freetext_editor_spec.js b/test/integration/freetext_editor_spec.js index 37f904bd5..50443cfbb 100644 --- a/test/integration/freetext_editor_spec.js +++ b/test/integration/freetext_editor_spec.js @@ -599,6 +599,49 @@ describe("Editor", () => { [0, 0, 0], [0, 0, 0], ]); + + // Increase the font size for all the annotations. + // Select all. + await page.keyboard.down("Control"); + await page.keyboard.press("a"); + await page.keyboard.up("Control"); + await page.waitForTimeout(10); + + page.evaluate(() => { + window.PDFViewerApplication.eventBus.dispatch( + "switchannotationeditorparams", + { + source: null, + type: /* AnnotationEditorParamsType.FREETEXT_SIZE */ 1, + value: 13, + } + ); + }); + + await page.waitForTimeout(10); + expect(await serialize("fontSize")) + .withContext(`In ${browserName}`) + .toEqual([13, 13]); + + // Change the colors for all the annotations. + page.evaluate(() => { + window.PDFViewerApplication.eventBus.dispatch( + "switchannotationeditorparams", + { + source: null, + type: /* AnnotationEditorParamsType.FREETEXT_COLOR */ 2, + value: "#FF0000", + } + ); + }); + + await page.waitForTimeout(10); + expect(await serialize("color")) + .withContext(`In ${browserName}`) + .toEqual([ + [255, 0, 0], + [255, 0, 0], + ]); }) ); }); diff --git a/web/default_factory.js b/web/default_factory.js index 0688eee58..3d93b5474 100644 --- a/web/default_factory.js +++ b/web/default_factory.js @@ -111,9 +111,7 @@ class DefaultAnnotationEditorLayerFactory { * @property {HTMLDivElement} pageDiv * @property {PDFPageProxy} pdfPage * @property {IL10n} l10n - * @property {AnnotationStorage} [annotationStorage] - Storage for annotation * @property {TextAccessibilityManager} [accessibilityManager] - * data in forms. */ /** @@ -126,7 +124,6 @@ class DefaultAnnotationEditorLayerFactory { pdfPage, accessibilityManager = null, l10n, - annotationStorage = null, }) { return new AnnotationEditorLayerBuilder({ uiManager, @@ -134,7 +131,6 @@ class DefaultAnnotationEditorLayerFactory { pdfPage, accessibilityManager, l10n, - annotationStorage, }); } } diff --git a/web/interfaces.js b/web/interfaces.js index e558cb39b..b96c7fcdc 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -237,9 +237,7 @@ class IPDFAnnotationEditorLayerFactory { * @property {HTMLDivElement} pageDiv * @property {PDFPageProxy} pdfPage * @property {IL10n} l10n - * @property {AnnotationStorage} [annotationStorage] - Storage for annotation * @property {TextAccessibilityManager} [accessibilityManager] - * data in forms. */ /** @@ -251,7 +249,6 @@ class IPDFAnnotationEditorLayerFactory { pageDiv, pdfPage, l10n, - annotationStorage = null, accessibilityManager, }) {} } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index d6ab1ae3f..142b5e0fe 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -730,7 +730,8 @@ class PDFViewer { } else if (isValidAnnotationEditorMode(mode)) { this.#annotationEditorUIManager = new AnnotationEditorUIManager( this.container, - this.eventBus + this.eventBus, + this.pdfDocument?.annotationStorage ); if (mode !== AnnotationEditorType.NONE) { this.#annotationEditorUIManager.updateMode(mode); @@ -1741,9 +1742,7 @@ class PDFViewer { * @property {HTMLDivElement} pageDiv * @property {PDFPageProxy} pdfPage * @property {IL10n} l10n - * @property {AnnotationStorage} [annotationStorage] - Storage for annotation * @property {TextAccessibilityManager} [accessibilityManager] - * data in forms. */ /** @@ -1756,13 +1755,11 @@ class PDFViewer { pdfPage, accessibilityManager = null, l10n, - annotationStorage = this.pdfDocument?.annotationStorage, }) { return new AnnotationEditorLayerBuilder({ uiManager, pageDiv, pdfPage, - annotationStorage, accessibilityManager, l10n, });