From 2df2defa027e799a1d5bcd5f7f7af4755cd5bcc6 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 12 Jul 2022 17:32:14 +0200 Subject: [PATCH] [Editor] Always have an ink editor (when in ink mode) Previously it was created only on mouseover event but on a touch screen there are no fingerover event... The idea behind creating the ink editor on mouseover was to avoid to have a canvas on each visible page. So now, when the editor is created, the canvas has dimensions 1x1 and only when the user starts drawing the dimensions are set to the page ones. --- src/display/annotation_storage.js | 6 +- src/display/editor/annotation_editor_layer.js | 93 ++++++++++--------- src/display/editor/editor.js | 11 ++- src/display/editor/freetext.js | 4 + src/display/editor/ink.js | 32 +++++-- src/display/editor/tools.js | 4 +- 6 files changed, 97 insertions(+), 53 deletions(-) diff --git a/src/display/annotation_storage.js b/src/display/annotation_storage.js index 3729192ee..515869c13 100644 --- a/src/display/annotation_storage.js +++ b/src/display/annotation_storage.js @@ -146,7 +146,11 @@ class AnnotationStorage { const clone = new Map(); for (const [key, val] of this._storage) { - clone.set(key, val instanceof AnnotationEditor ? val.serialize() : val); + const serialized = + val instanceof AnnotationEditor ? val.serialize() : val; + if (serialized) { + clone.set(key, serialized); + } } return clone; } diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 6b5380842..ecc29b3f7 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -42,10 +42,10 @@ import { InkEditor } from "./ink.js"; class AnnotationEditorLayer { #boundClick; - #boundMouseover; - #editors = new Map(); + #isCleaningUp = false; + #uiManager; static _initialized = false; @@ -92,7 +92,6 @@ class AnnotationEditorLayer { this.pageIndex = options.pageIndex; this.div = options.div; this.#boundClick = this.click.bind(this); - this.#boundMouseover = this.mouseover.bind(this); for (const editor of this.#uiManager.getEditors(options.pageIndex)) { this.add(editor); @@ -114,24 +113,36 @@ class AnnotationEditorLayer { * The mode has changed: it must be updated. * @param {number} mode */ - updateMode(mode) { - switch (mode) { - case AnnotationEditorType.INK: - // We want to have the ink editor covering all of the page without - // having to click to create it: it must be here when we start to draw. - this.div.addEventListener("mouseover", this.#boundMouseover); - this.div.removeEventListener("click", this.#boundClick); - break; - case AnnotationEditorType.FREETEXT: - this.div.removeEventListener("mouseover", this.#boundMouseover); - this.div.addEventListener("click", this.#boundClick); - break; - default: - this.div.removeEventListener("mouseover", this.#boundMouseover); - this.div.removeEventListener("click", this.#boundClick); + updateMode(mode = this.#uiManager.getMode()) { + this.#cleanup(); + if (mode === AnnotationEditorType.INK) { + // We always want to an ink editor ready to draw in. + this.addInkEditorIfNeeded(false); + } + this.setActiveEditor(null); + } + + addInkEditorIfNeeded(isCommitting) { + if ( + !isCommitting && + this.#uiManager.getMode() !== AnnotationEditorType.INK + ) { + return; } - this.setActiveEditor(null); + if (!isCommitting) { + // We're removing an editor but an empty one can already exist so in this + // case we don't need to create a new one. + for (const editor of this.#editors.values()) { + if (editor.isEmpty()) { + editor.setInBackground(); + return; + } + } + } + + const editor = this.#createAndAddNewEditor({ offsetX: 0, offsetY: 0 }); + editor.setInBackground(); } /** @@ -142,25 +153,6 @@ class AnnotationEditorLayer { this.#uiManager.setEditingState(isEditing); } - /** - * Mouseover callback. - * @param {MouseEvent} event - */ - mouseover(event) { - if ( - event.target === this.div && - event.buttons === 0 && - !this.#uiManager.hasActive() - ) { - // The div is the target so there is no ink editor, hence we can - // create a new one. - // event.buttons === 0 is here to avoid adding a new ink editor - // when we drop an editor. - const editor = this.#createAndAddNewEditor(event); - editor.setInBackground(); - } - } - /** * Add some commands into the CommandManager (undo/redo stuff). * @param {Object} params @@ -258,14 +250,12 @@ class AnnotationEditorLayer { currentActive.commitOrRemove(); } + this.#uiManager.allowClick = + this.#uiManager.getMode() === AnnotationEditorType.INK; if (editor) { this.unselectAll(); this.div.removeEventListener("click", this.#boundClick); } else { - // When in Ink mode, setting the editor to null allows the - // user to have to make one click in order to start drawing. - this.#uiManager.allowClick = - this.#uiManager.getMode() === AnnotationEditorType.INK; this.div.addEventListener("click", this.#boundClick); } } @@ -295,6 +285,10 @@ class AnnotationEditorLayer { this.setActiveEditor(null); this.#uiManager.allowClick = true; } + + if (!this.#isCleaningUp) { + this.addInkEditorIfNeeded(/* isCommitting = */ false); + } } /** @@ -496,6 +490,19 @@ class AnnotationEditorLayer { this.#uiManager.removeLayer(this); } + #cleanup() { + // When we're cleaning up, some editors are removed but we don't want + // to add a new one which will induce an addition in this.#editors, hence + // an infinite loop. + this.#isCleaningUp = true; + for (const editor of this.#editors.values()) { + if (editor.isEmpty()) { + editor.remove(); + } + } + this.#isCleaningUp = false; + } + /** * Render the main editor. * @param {Object} parameters @@ -505,6 +512,7 @@ class AnnotationEditorLayer { bindEvents(this, this.div, ["dragover", "drop", "keydown"]); this.div.addEventListener("click", this.#boundClick); this.setDimensions(); + this.updateMode(); } /** @@ -515,6 +523,7 @@ class AnnotationEditorLayer { this.setActiveEditor(null); this.viewport = parameters.viewport; this.setDimensions(); + this.updateMode(); } /** diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index eb91a39b5..110e8464d 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -16,8 +16,12 @@ // eslint-disable-next-line max-len /** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */ +import { + AnnotationEditorPrefix, + shadow, + unreachable, +} from "../../shared/util.js"; import { bindEvents, ColorManager } from "./tools.js"; -import { shadow, unreachable } from "../../shared/util.js"; /** * @typedef {Object} AnnotationEditorParameters @@ -109,7 +113,10 @@ class AnnotationEditor { event.preventDefault(); this.commitOrRemove(); - this.parent.setActiveEditor(null); + + if (!target?.id?.startsWith(AnnotationEditorPrefix)) { + this.parent.setActiveEditor(null); + } } commitOrRemove() { diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 85f99ec7c..0bada5621 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -372,6 +372,10 @@ class FreeTextEditor extends AnnotationEditor { /** @inheritdoc */ serialize() { + if (this.isEmpty()) { + return null; + } + const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor; const rect = this.getRect(padding, padding); diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 559cf679d..3c60a65a1 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -41,6 +41,8 @@ class InkEditor extends AnnotationEditor { #disableEditing = false; + #isCanvasInitialized = false; + #observer = null; #realWidth = 0; @@ -53,11 +55,8 @@ class InkEditor extends AnnotationEditor { constructor(params) { super({ ...params, name: "inkEditor" }); - this.color = - params.color || - InkEditor._defaultColor || - AnnotationEditor._defaultLineColor; - this.thickness = params.thickness || InkEditor._defaultThickness; + this.color = params.color || null; + this.thickness = params.thickness || null; this.paths = []; this.bezierPath2D = []; this.currentPath = []; @@ -255,7 +254,6 @@ class InkEditor extends AnnotationEditor { /** @inheritdoc */ onceAdded() { this.div.draggable = !this.isEmpty(); - this.div.focus(); } /** @inheritdoc */ @@ -298,6 +296,13 @@ class InkEditor extends AnnotationEditor { * @param {number} y */ #startDrawing(x, y) { + if (!this.#isCanvasInitialized) { + this.#isCanvasInitialized = true; + this.#setCanvasDims(); + this.thickness ||= InkEditor._defaultThickness; + this.color ||= + InkEditor._defaultColor || AnnotationEditor._defaultLineColor; + } this.currentPath.push([x, y]); this.#setStroke(); this.ctx.beginPath(); @@ -406,6 +411,8 @@ class InkEditor extends AnnotationEditor { this.div.classList.add("disabled"); this.#fitToContent(); + + this.parent.addInkEditorIfNeeded(/* isCommitting = */ true); } /** @inheritdoc */ @@ -491,6 +498,7 @@ class InkEditor extends AnnotationEditor { */ #createCanvas() { this.canvas = document.createElement("canvas"); + this.canvas.width = this.canvas.height = 0; this.canvas.className = "inkEditorCanvas"; this.div.append(this.canvas); this.ctx = this.canvas.getContext("2d"); @@ -522,7 +530,6 @@ class InkEditor extends AnnotationEditor { } super.render(); - this.div.classList.add("editing"); const [x, y, w, h] = this.#getInitialBBox(); this.setAt(x, y, 0, 0); this.setDims(w, h); @@ -531,6 +538,7 @@ class InkEditor extends AnnotationEditor { if (this.width) { // This editor was created in using copy (ctrl+c). + this.#isCanvasInitialized = true; const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; this.setAt( baseX * parentWidth, @@ -542,6 +550,9 @@ class InkEditor extends AnnotationEditor { this.#setCanvasDims(); this.#redraw(); this.div.classList.add("disabled"); + } else { + this.div.classList.add("editing"); + this.enableEditMode(); } this.#createObserver(); @@ -550,6 +561,9 @@ class InkEditor extends AnnotationEditor { } #setCanvasDims() { + if (!this.#isCanvasInitialized) { + return; + } const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions; this.canvas.width = this.width * parentWidth; this.canvas.height = this.height * parentHeight; @@ -861,6 +875,10 @@ class InkEditor extends AnnotationEditor { /** @inheritdoc */ serialize() { + if (this.isEmpty()) { + return null; + } + const rect = this.getRect(0, 0); const height = this.rotation % 180 === 0 ? rect[3] - rect[1] : rect[2] - rect[0]; diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 22f92ec9e..83d8696c6 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -779,7 +779,9 @@ class AnnotationEditorUIManager { const editors = Array.from(this.#allEditors.values()); cmd = () => { for (const editor of editors) { - editor.remove(); + if (!editor.isEmpty()) { + editor.remove(); + } } };