Merge pull request #15050 from calixteman/make_ink_better

[Editor] - Add the ability to directly draw after selecting ink tool
This commit is contained in:
calixteman 2022-06-16 20:34:56 +02:00 committed by GitHub
commit b8688128e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 162 additions and 39 deletions

View File

@ -43,6 +43,8 @@ import { PixelsPerInch } from "../display_utils.js";
class AnnotationEditorLayer { class AnnotationEditorLayer {
#boundClick; #boundClick;
#boundMouseover;
#editors = new Map(); #editors = new Map();
#uiManager; #uiManager;
@ -83,6 +85,7 @@ class AnnotationEditorLayer {
this.pageIndex = options.pageIndex; this.pageIndex = options.pageIndex;
this.div = options.div; this.div = options.div;
this.#boundClick = this.click.bind(this); this.#boundClick = this.click.bind(this);
this.#boundMouseover = this.mouseover.bind(this);
for (const editor of this.#uiManager.getEditors(options.pageIndex)) { for (const editor of this.#uiManager.getEditors(options.pageIndex)) {
this.add(editor); this.add(editor);
@ -91,13 +94,45 @@ class AnnotationEditorLayer {
this.#uiManager.addLayer(this); this.#uiManager.addLayer(this);
} }
/**
* The mode has changed: it must be updated.
* @param {number} mode
*/
updateMode(mode) {
if (mode === 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);
} else {
this.div.removeEventListener("mouseover", this.#boundMouseover);
}
}
/**
* Mouseover callback.
* @param {MouseEvent} event
*/
mouseover(event) {
if (event.target === this.div && event.buttons === 0) {
// 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). * Add some commands into the CommandManager (undo/redo stuff).
* @param {function} cmd * @param {function} cmd
* @param {function} undo * @param {function} undo
* @param {boolean} mustExec - If true the command is executed after having
* been added.
*/ */
addCommands(cmd, undo) { addCommands(cmd, undo, mustExec) {
this.#uiManager.addCommands(cmd, undo); this.#uiManager.addCommands(cmd, undo, mustExec);
} }
/** /**
@ -178,6 +213,17 @@ class AnnotationEditorLayer {
* @param {AnnotationEditor} editor * @param {AnnotationEditor} editor
*/ */
setActiveEditor(editor) { setActiveEditor(editor) {
const currentActive = this.#uiManager.getActive();
if (currentActive === editor) {
return;
}
this.#uiManager.setActiveEditor(editor);
if (currentActive && currentActive !== editor) {
currentActive.commitOrRemove();
}
if (editor) { if (editor) {
this.unselectAll(); this.unselectAll();
this.div.removeEventListener("click", this.#boundClick); this.div.removeEventListener("click", this.#boundClick);
@ -185,7 +231,6 @@ class AnnotationEditorLayer {
this.#uiManager.allowClick = false; this.#uiManager.allowClick = false;
this.div.addEventListener("click", this.#boundClick); this.div.addEventListener("click", this.#boundClick);
} }
this.#uiManager.setActiveEditor(editor);
} }
attach(editor) { attach(editor) {
@ -212,7 +257,6 @@ class AnnotationEditorLayer {
if (this.#uiManager.isActive(editor) || this.#editors.size === 0) { if (this.#uiManager.isActive(editor) || this.#editors.size === 0) {
this.setActiveEditor(null); this.setActiveEditor(null);
this.#uiManager.allowClick = true; this.#uiManager.allowClick = true;
this.div.focus();
} }
} }
@ -279,7 +323,22 @@ class AnnotationEditorLayer {
editor.remove(); editor.remove();
}; };
this.addCommands(cmd, undo); this.addCommands(cmd, undo, true);
}
/**
* Add a new editor and make this addition undoable.
* @param {AnnotationEditor} editor
*/
addUndoableEditor(editor) {
const cmd = () => {
this.addOrRebuild(editor);
};
const undo = () => {
editor.remove();
};
this.addCommands(cmd, undo, false);
} }
/** /**
@ -305,6 +364,26 @@ class AnnotationEditorLayer {
return null; return null;
} }
/**
* Create and add a new editor.
* @param {MouseEvent} event
* @returns {AnnotationEditor}
*/
#createAndAddNewEditor(event) {
const id = this.getNextId();
const editor = this.#createNewEditor({
parent: this,
id,
x: event.offsetX,
y: event.offsetY,
});
if (editor) {
this.add(editor);
}
return editor;
}
/** /**
* Mouseclick callback. * Mouseclick callback.
* @param {MouseEvent} event * @param {MouseEvent} event
@ -316,16 +395,7 @@ class AnnotationEditorLayer {
return; return;
} }
const id = this.getNextId(); this.#createAndAddNewEditor(event);
const editor = this.#createNewEditor({
parent: this,
id,
x: event.offsetX,
y: event.offsetY,
});
if (editor) {
this.addANewEditor(editor);
}
} }
/** /**

View File

@ -53,6 +53,20 @@ class AnnotationEditor {
this.isAttachedToDOM = false; this.isAttachedToDOM = false;
} }
/**
* This editor will be behind the others.
*/
setInBackground() {
this.div.classList.add("background");
}
/**
* This editor will be in the foreground.
*/
setInForeground() {
this.div.classList.remove("background");
}
/** /**
* onfocus callback. * onfocus callback.
*/ */
@ -81,12 +95,16 @@ class AnnotationEditor {
event.preventDefault(); event.preventDefault();
this.commitOrRemove();
this.parent.setActiveEditor(null);
}
commitOrRemove() {
if (this.isEmpty()) { if (this.isEmpty()) {
this.remove(); this.remove();
} else { } else {
this.commit(); this.commit();
} }
this.parent.setActiveEditor(null);
} }
/** /**
@ -156,7 +174,6 @@ class AnnotationEditor {
this.div = document.createElement("div"); this.div = document.createElement("div");
this.div.className = this.name; this.div.className = this.name;
this.div.setAttribute("id", this.id); this.div.setAttribute("id", this.id);
this.div.draggable = true;
this.div.tabIndex = 100; this.div.tabIndex = 100;
const [tx, ty] = this.getInitialTranslation(); const [tx, ty] = this.getInitialTranslation();

View File

@ -33,6 +33,8 @@ class FreeTextEditor extends AnnotationEditor {
#contentHTML = ""; #contentHTML = "";
#hasAlreadyBeenCommitted = false;
#fontSize; #fontSize;
static _freeTextDefaultContent = ""; static _freeTextDefaultContent = "";
@ -168,6 +170,13 @@ class FreeTextEditor extends AnnotationEditor {
* @returns {undefined} * @returns {undefined}
*/ */
commit() { commit() {
if (!this.#hasAlreadyBeenCommitted) {
// This editor has something and it's the first time
// it's commited so we can it in the undo/redo stack.
this.#hasAlreadyBeenCommitted = true;
this.parent.addUndoableEditor(this);
}
this.disableEditMode(); this.disableEditMode();
this.#contentHTML = this.editorDiv.innerHTML; this.#contentHTML = this.editorDiv.innerHTML;
this.#content = this.#extractText().trimEnd(); this.#content = this.#extractText().trimEnd();

View File

@ -21,11 +21,11 @@ import { fitCurve } from "./fit_curve/fit_curve.js";
* Basic draw editor in order to generate an Ink annotation. * Basic draw editor in order to generate an Ink annotation.
*/ */
class InkEditor extends AnnotationEditor { class InkEditor extends AnnotationEditor {
#aspectRatio; #aspectRatio = 0;
#baseHeight; #baseHeight = 0;
#baseWidth; #baseWidth = 0;
#boundCanvasMousemove; #boundCanvasMousemove;
@ -35,9 +35,9 @@ class InkEditor extends AnnotationEditor {
#boundCanvasMousedown; #boundCanvasMousedown;
#disableEditing; #disableEditing = false;
#observer; #observer = null;
constructor(params) { constructor(params) {
super({ ...params, name: "inkEditor" }); super({ ...params, name: "inkEditor" });
@ -48,10 +48,6 @@ class InkEditor extends AnnotationEditor {
this.currentPath = []; this.currentPath = [];
this.scaleFactor = 1; this.scaleFactor = 1;
this.translationX = this.translationY = 0; this.translationX = this.translationY = 0;
this.#baseWidth = this.#baseHeight = 0;
this.#aspectRatio = 0;
this.#disableEditing = false;
this.#observer = null;
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
@ -113,8 +109,6 @@ class InkEditor extends AnnotationEditor {
return; return;
} }
super.remove();
// Destroy the canvas. // Destroy the canvas.
this.canvas.width = this.canvas.heigth = 0; this.canvas.width = this.canvas.heigth = 0;
this.canvas.remove(); this.canvas.remove();
@ -122,11 +116,13 @@ class InkEditor extends AnnotationEditor {
this.#observer.disconnect(); this.#observer.disconnect();
this.#observer = null; this.#observer = null;
super.remove();
} }
/** @inheritdoc */ /** @inheritdoc */
enableEditMode() { enableEditMode() {
if (this.#disableEditing) { if (this.#disableEditing || this.canvas === null) {
return; return;
} }
@ -145,7 +141,7 @@ class InkEditor extends AnnotationEditor {
super.disableEditMode(); super.disableEditMode();
this.canvas.style.cursor = "auto"; this.canvas.style.cursor = "auto";
this.div.draggable = true; this.div.draggable = !this.isEmpty();
this.div.classList.remove("editing"); this.div.classList.remove("editing");
this.canvas.removeEventListener("mousedown", this.#boundCanvasMousedown); this.canvas.removeEventListener("mousedown", this.#boundCanvasMousedown);
@ -154,6 +150,7 @@ class InkEditor extends AnnotationEditor {
/** @inheritdoc */ /** @inheritdoc */
onceAdded() { onceAdded() {
this.div.draggable = !this.isEmpty();
this.div.focus(); this.div.focus();
} }
@ -238,11 +235,15 @@ class InkEditor extends AnnotationEditor {
if (this.paths.length === 0) { if (this.paths.length === 0) {
this.remove(); this.remove();
} else { } else {
if (!this.canvas) {
this.#createCanvas();
this.#createObserver();
}
this.#fitToContent(); this.#fitToContent();
} }
}; };
this.parent.addCommands(cmd, undo); this.parent.addCommands(cmd, undo, true);
} }
/** /**
@ -273,8 +274,12 @@ class InkEditor extends AnnotationEditor {
if (this.#disableEditing) { if (this.#disableEditing) {
return; return;
} }
this.disableEditMode(); this.disableEditMode();
// This editor must be on top of the main ink editor.
this.setInForeground();
this.#disableEditing = true; this.#disableEditing = true;
this.div.classList.add("disabled"); this.div.classList.add("disabled");
@ -297,6 +302,10 @@ class InkEditor extends AnnotationEditor {
return; return;
} }
// We want to draw on top of any other editors.
// Since it's the last child, there's no need to give it a higher z-index.
this.setInForeground();
event.stopPropagation(); event.stopPropagation();
this.canvas.addEventListener("mouseleave", this.#boundCanvasMouseleave); this.canvas.addEventListener("mouseleave", this.#boundCanvasMouseleave);
@ -324,6 +333,10 @@ class InkEditor extends AnnotationEditor {
if (this.isInEditMode() && this.currentPath.length !== 0) { if (this.isInEditMode() && this.currentPath.length !== 0) {
event.stopPropagation(); event.stopPropagation();
this.#endDrawing(event); this.#endDrawing(event);
// Since the ink editor covers all of the page and we want to be able
// to select another editor, we just put this one in the background.
this.setInBackground();
} }
} }
@ -334,6 +347,7 @@ class InkEditor extends AnnotationEditor {
*/ */
canvasMouseleave(event) { canvasMouseleave(event) {
this.#endDrawing(event); this.#endDrawing(event);
this.setInBackground();
} }
/** /**

View File

@ -63,8 +63,9 @@ class CommandManager {
* Add a new couple of commands to be used in case of redo/undo. * Add a new couple of commands to be used in case of redo/undo.
* @param {function} cmd * @param {function} cmd
* @param {function} undo * @param {function} undo
* @param {boolean} mustExec
*/ */
add(cmd, undo) { add(cmd, undo, mustExec) {
const save = [cmd, undo]; const save = [cmd, undo];
const next = (this.#position + 1) % this.#maxSize; const next = (this.#position + 1) % this.#maxSize;
if (next !== this.#start) { if (next !== this.#start) {
@ -79,7 +80,10 @@ class CommandManager {
this.#position = this.#commands.length - 1; this.#position = this.#commands.length - 1;
} }
this.#setCommands(save); this.#setCommands(save);
cmd();
if (mustExec) {
cmd();
}
} }
/** /**
@ -316,6 +320,9 @@ class AnnotationEditorUIManager {
this.#disableAll(); this.#disableAll();
} else { } else {
this.#enableAll(); this.#enableAll();
for (const layer of this.#allLayers) {
layer.updateMode(mode);
}
} }
} }
@ -409,9 +416,10 @@ class AnnotationEditorUIManager {
* Add a command to execute (cmd) and another one to undo it. * Add a command to execute (cmd) and another one to undo it.
* @param {function} cmd * @param {function} cmd
* @param {function} undo * @param {function} undo
* @param {boolean} mustExec
*/ */
addCommands(cmd, undo) { addCommands(cmd, undo, mustExec) {
this.#commandManager.add(cmd, undo); this.#commandManager.add(cmd, undo, mustExec);
} }
/** /**
@ -460,7 +468,7 @@ class AnnotationEditorUIManager {
} }
}; };
this.addCommands(cmd, undo); this.addCommands(cmd, undo, true);
} else { } else {
if (!this.#activeEditor) { if (!this.#activeEditor) {
return; return;
@ -474,7 +482,7 @@ class AnnotationEditorUIManager {
}; };
} }
this.addCommands(cmd, undo); this.addCommands(cmd, undo, true);
} }
/** /**
@ -501,7 +509,7 @@ class AnnotationEditorUIManager {
layer.addOrRebuild(editor); layer.addOrRebuild(editor);
}; };
this.addCommands(cmd, undo); this.addCommands(cmd, undo, true);
} }
} }
@ -522,7 +530,7 @@ class AnnotationEditorUIManager {
editor.remove(); editor.remove();
}; };
this.addCommands(cmd, undo); this.addCommands(cmd, undo, true);
} }
/** /**

View File

@ -98,6 +98,11 @@
overflow: auto; overflow: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 1;
}
.annotationEditorLayer .background {
z-index: 0;
} }
.annotationEditorLayer .inkEditor:focus { .annotationEditorLayer .inkEditor:focus {