Merge pull request #15130 from calixteman/context_menu

[Editor] Dispatch an event when some global states are changing (bug 1777695)
This commit is contained in:
Jonas Jenwald 2022-07-05 22:40:12 +02:00 committed by GitHub
commit bde46632d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 278 additions and 28 deletions

View File

@ -67,7 +67,7 @@ class AnnotationEditorLayer {
"mac+ctrl+Backspace",
"mac+alt+Backspace",
],
AnnotationEditorLayer.prototype.suppress,
AnnotationEditorLayer.prototype.delete,
],
]);
@ -128,6 +128,14 @@ class AnnotationEditorLayer {
this.setActiveEditor(null);
}
/**
* Set the editing state.
* @param {boolean} isEditing
*/
setEditingState(isEditing) {
this.#uiManager.setEditingState(isEditing);
}
/**
* Mouseover callback.
* @param {MouseEvent} event
@ -173,8 +181,8 @@ class AnnotationEditorLayer {
* Suppress the selected editor or all editors.
* @returns {undefined}
*/
suppress() {
this.#uiManager.suppress();
delete() {
this.#uiManager.delete();
}
/**
@ -188,7 +196,7 @@ class AnnotationEditorLayer {
* Cut the selected editor.
*/
cut() {
this.#uiManager.cut(this);
this.#uiManager.cut();
}
/**
@ -196,7 +204,7 @@ class AnnotationEditorLayer {
* @returns {undefined}
*/
paste() {
this.#uiManager.paste(this);
this.#uiManager.paste();
}
/**

View File

@ -218,11 +218,27 @@ class AnnotationEditor {
const [tx, ty] = this.getInitialTranslation();
this.translate(tx, ty);
bindEvents(this, this.div, ["dragstart", "focusin", "focusout"]);
bindEvents(this, this.div, [
"dragstart",
"focusin",
"focusout",
"mousedown",
]);
return this.div;
}
/**
* Onmousedown callback.
* @param {MouseEvent} event
*/
mousedown(event) {
if (event.button !== 0) {
// Avoid to focus this editor because of a non-left click.
event.preventDefault();
}
}
getRect(tx, ty) {
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
const [pageWidth, pageHeight] = this.parent.pageDimensions;
@ -362,6 +378,11 @@ class AnnotationEditor {
* @returns {undefined}
*/
remove() {
if (!this.isEmpty()) {
// The editor is removed but it can be back at some point thanks to
// undo/redo so we must commit it before.
this.commit();
}
this.parent.remove(this);
}

View File

@ -214,6 +214,7 @@ class FreeTextEditor extends AnnotationEditor {
/** @inheritdoc */
enableEditMode() {
this.parent.setEditingState(false);
this.parent.updateToolbar(AnnotationEditorType.FREETEXT);
super.enableEditMode();
this.overlayDiv.classList.remove("enabled");
@ -223,6 +224,7 @@ class FreeTextEditor extends AnnotationEditor {
/** @inheritdoc */
disableEditMode() {
this.parent.setEditingState(true);
super.disableEditMode();
this.overlayDiv.classList.add("enabled");
this.editorDiv.contentEditable = false;
@ -245,6 +247,12 @@ class FreeTextEditor extends AnnotationEditor {
return this.editorDiv.innerText.trim() === "";
}
/** @inheritdoc */
remove() {
this.parent.setEditingState(true);
super.remove();
}
/**
* Extract the text from this editor.
* @returns {string}
@ -282,7 +290,7 @@ class FreeTextEditor extends AnnotationEditor {
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.
// it's commited so we can add it in the undo/redo stack.
this.#hasAlreadyBeenCommitted = true;
this.parent.addUndoableEditor(this);
}

View File

@ -211,8 +211,12 @@ class InkEditor extends AnnotationEditor {
return;
}
if (!this.isEmpty()) {
this.commit();
}
// Destroy the canvas.
this.canvas.width = this.canvas.heigth = 0;
this.canvas.width = this.canvas.height = 0;
this.canvas.remove();
this.canvas = null;
@ -258,7 +262,10 @@ class InkEditor extends AnnotationEditor {
/** @inheritdoc */
isEmpty() {
return this.paths.length === 0;
return (
this.paths.length === 0 ||
(this.paths.length === 1 && this.paths[0].length === 0)
);
}
#getInitialBBox() {
@ -415,7 +422,7 @@ class InkEditor extends AnnotationEditor {
* @returns {undefined}
*/
canvasMousedown(event) {
if (!this.isInEditMode() || this.#disableEditing) {
if (event.button !== 0 || !this.isInEditMode() || this.#disableEditing) {
return;
}
@ -447,6 +454,9 @@ class InkEditor extends AnnotationEditor {
* @returns {undefined}
*/
canvasMouseup(event) {
if (event.button !== 0) {
return;
}
if (this.isInEditMode() && this.currentPath.length !== 0) {
event.stopPropagation();
this.#endDrawing(event);

View File

@ -150,6 +150,26 @@ class CommandManager {
}
}
/**
* Check if there is something to undo.
* @returns {boolean}
*/
hasSomethingToUndo() {
return !isNaN(this.#position);
}
/**
* Check if there is something to redo.
* @returns {boolean}
*/
hasSomethingToRedo() {
if (isNaN(this.#position) && this.#start < this.#commands.length) {
return true;
}
const next = (this.#position + 1) % this.#maxSize;
return next !== this.#start && next < this.#commands.length;
}
#setCommands(cmds) {
if (this.#commands.length < this.#maxSize) {
this.#commands.push(cmds);
@ -167,6 +187,10 @@ class CommandManager {
}
this.#commands[this.#position] = cmds;
}
destroy() {
this.#commands = null;
}
}
/**
@ -279,6 +303,18 @@ class ClipboardManager {
paste() {
return this.element?.copy() || null;
}
/**
* Check if the clipboard is empty.
* @returns {boolean}
*/
isEmpty() {
return this.element === null;
}
destroy() {
this.element = null;
}
}
class ColorManager {
@ -355,7 +391,7 @@ class AnnotationEditorUIManager {
#allEditors = new Map();
#allLayers = new Set();
#allLayers = new Map();
#allowClick = true;
@ -377,8 +413,69 @@ class AnnotationEditorUIManager {
#previousActiveEditor = null;
#boundOnEditingAction = this.onEditingAction.bind(this);
#previousStates = {
isEditing: false,
isEmpty: true,
hasEmptyClipboard: true,
hasSomethingToUndo: false,
hasSomethingToRedo: false,
hasSelectedEditor: false,
};
constructor(eventBus) {
this.#eventBus = eventBus;
this.#eventBus._on("editingaction", this.#boundOnEditingAction);
}
destroy() {
this.#eventBus._off("editingaction", this.#boundOnEditingAction);
for (const layer of this.#allLayers.values()) {
layer.destroy();
}
this.#allLayers.clear();
for (const editor of this.#allEditors.values()) {
editor.destroy();
}
this.#allEditors.clear();
this.#activeEditor = null;
this.#clipboardManager.destroy();
this.#commandManager.destroy();
}
/**
* Execute an action for a given name.
* For example, the user can click on the "Undo" entry in the context menu
* and it'll trigger the undo action.
* @param {Object} details
*/
onEditingAction(details) {
if (
["undo", "redo", "cut", "copy", "paste", "delete", "selectAll"].includes(
details.name
)
) {
this[details.name]();
}
}
/**
* Update the different possible states of this manager, e.g. is the clipboard
* empty or is there something to undo, ...
* @param {Object} details
*/
#dispatchUpdateStates(details) {
const hasChanged = Object.entries(details).some(
([key, value]) => this.#previousStates[key] !== value
);
if (hasChanged) {
this.#eventBus.dispatch("annotationeditorstateschanged", {
source: this,
details: Object.assign(this.#previousStates, details),
});
}
}
#dispatchUpdateUI(details) {
@ -388,6 +485,29 @@ class AnnotationEditorUIManager {
});
}
/**
* Set the editing state.
* It can be useful to temporarily disable it when the user is editing a
* FreeText annotation.
* @param {boolean} isEditing
*/
setEditingState(isEditing) {
if (isEditing) {
this.#dispatchUpdateStates({
isEditing: this.#mode !== AnnotationEditorType.NONE,
isEmpty: this.#isEmpty(),
hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(),
hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(),
hasSelectedEditor: false,
hasEmptyClipboard: this.#clipboardManager.isEmpty(),
});
} else {
this.#dispatchUpdateStates({
isEditing: false,
});
}
}
registerEditorTypes(types) {
this.#editorTypes = types;
for (const editorType of this.#editorTypes) {
@ -408,7 +528,7 @@ class AnnotationEditorUIManager {
* @param {AnnotationEditorLayer} layer
*/
addLayer(layer) {
this.#allLayers.add(layer);
this.#allLayers.set(layer.pageIndex, layer);
if (this.#isEnabled) {
layer.enable();
} else {
@ -421,7 +541,7 @@ class AnnotationEditorUIManager {
* @param {AnnotationEditorLayer} layer
*/
removeLayer(layer) {
this.#allLayers.delete(layer);
this.#allLayers.delete(layer.pageIndex);
}
/**
@ -431,10 +551,12 @@ class AnnotationEditorUIManager {
updateMode(mode) {
this.#mode = mode;
if (mode === AnnotationEditorType.NONE) {
this.setEditingState(false);
this.#disableAll();
} else {
this.setEditingState(true);
this.#enableAll();
for (const layer of this.#allLayers) {
for (const layer of this.#allLayers.values()) {
layer.updateMode(mode);
}
}
@ -476,7 +598,7 @@ class AnnotationEditorUIManager {
#enableAll() {
if (!this.#isEnabled) {
this.#isEnabled = true;
for (const layer of this.#allLayers) {
for (const layer of this.#allLayers.values()) {
layer.enable();
}
}
@ -488,7 +610,7 @@ class AnnotationEditorUIManager {
#disableAll() {
if (this.#isEnabled) {
this.#isEnabled = false;
for (const layer of this.#allLayers) {
for (const layer of this.#allLayers.values()) {
layer.disable();
}
}
@ -534,6 +656,19 @@ class AnnotationEditorUIManager {
this.#allEditors.delete(editor.id);
}
/**
* Add an editor to the layer it belongs to or add it to the global map.
* @param {AnnotationEditor} editor
*/
#addEditorToLayer(editor) {
const layer = this.#allLayers.get(editor.pageIndex);
if (layer) {
layer.addOrRebuild(editor);
} else {
this.addEditor(editor);
}
}
/**
* Set the given editor as the active one.
* @param {AnnotationEditor} editor
@ -548,7 +683,9 @@ class AnnotationEditorUIManager {
this.#activeEditor = editor;
if (editor) {
this.#dispatchUpdateUI(editor.propertiesToUpdate);
this.#dispatchUpdateStates({ hasSelectedEditor: true });
} else {
this.#dispatchUpdateStates({ hasSelectedEditor: false });
if (this.#previousActiveEditor) {
this.#dispatchUpdateUI(this.#previousActiveEditor.propertiesToUpdate);
} else {
@ -564,6 +701,11 @@ class AnnotationEditorUIManager {
*/
undo() {
this.#commandManager.undo();
this.#dispatchUpdateStates({
hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(),
hasSomethingToRedo: true,
isEmpty: this.#isEmpty(),
});
}
/**
@ -571,6 +713,11 @@ class AnnotationEditorUIManager {
*/
redo() {
this.#commandManager.redo();
this.#dispatchUpdateStates({
hasSomethingToUndo: true,
hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(),
isEmpty: this.#isEmpty(),
});
}
/**
@ -579,6 +726,25 @@ class AnnotationEditorUIManager {
*/
addCommands(params) {
this.#commandManager.add(params);
this.#dispatchUpdateStates({
hasSomethingToUndo: true,
hasSomethingToRedo: false,
isEmpty: this.#isEmpty(),
});
}
#isEmpty() {
if (this.#allEditors.size === 0) {
return true;
}
if (this.#allEditors.size === 1) {
for (const editor of this.#allEditors.values()) {
return editor.isEmpty();
}
}
return false;
}
/**
@ -608,10 +774,9 @@ class AnnotationEditorUIManager {
}
/**
* Suppress some editors from the given layer.
* @param {AnnotationEditorLayer} layer
* Delete the current editor or all.
*/
suppress(layer) {
delete() {
let cmd, undo;
if (this.#isAllSelected) {
const editors = Array.from(this.#allEditors.values());
@ -623,7 +788,7 @@ class AnnotationEditorUIManager {
undo = () => {
for (const editor of editors) {
layer.addOrRebuild(editor);
this.#addEditorToLayer(editor);
}
};
@ -637,7 +802,7 @@ class AnnotationEditorUIManager {
editor.remove();
};
undo = () => {
layer.addOrRebuild(editor);
this.#addEditorToLayer(editor);
};
}
@ -650,14 +815,14 @@ class AnnotationEditorUIManager {
copy() {
if (this.#activeEditor) {
this.#clipboardManager.copy(this.#activeEditor);
this.#dispatchUpdateStates({ hasEmptyClipboard: false });
}
}
/**
* Cut the selected editor.
* @param {AnnotationEditorLayer}
*/
cut(layer) {
cut() {
if (this.#activeEditor) {
this.#clipboardManager.copy(this.#activeEditor);
const editor = this.#activeEditor;
@ -665,7 +830,7 @@ class AnnotationEditorUIManager {
editor.remove();
};
const undo = () => {
layer.addOrRebuild(editor);
this.#addEditorToLayer(editor);
};
this.addCommands({ cmd, undo, mustExec: true });
@ -674,16 +839,16 @@ class AnnotationEditorUIManager {
/**
* Paste a previously copied editor.
* @param {AnnotationEditorLayer}
* @returns {undefined}
*/
paste(layer) {
paste() {
const editor = this.#clipboardManager.paste();
if (!editor) {
return;
}
// TODO: paste in the current visible layer.
const cmd = () => {
layer.addOrRebuild(editor);
this.#addEditorToLayer(editor);
};
const undo = () => {
editor.remove();
@ -700,6 +865,7 @@ class AnnotationEditorUIManager {
for (const editor of this.#allEditors.values()) {
editor.select();
}
this.#dispatchUpdateStates({ hasSelectedEditor: true });
}
/**
@ -707,9 +873,11 @@ class AnnotationEditorUIManager {
*/
unselectAll() {
this.#isAllSelected = false;
for (const editor of this.#allEditors.values()) {
editor.unselect();
}
this.#dispatchUpdateStates({ hasSelectedEditor: this.hasActive() });
}
/**

View File

@ -186,6 +186,10 @@ class DefaultExternalServices {
static get isInAutomation() {
return shadow(this, "isInAutomation", false);
}
static updateEditorStates(data) {
throw new Error("Not implemented: updateEditorStates");
}
}
const PDFViewerApplication = {
@ -1954,6 +1958,12 @@ const PDFViewerApplication = {
eventBus._on("fileinputchange", webViewerFileInputChange);
eventBus._on("openfile", webViewerOpenFile);
}
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
eventBus._on(
"annotationeditorstateschanged",
webViewerAnnotationEditorStatesChanged
);
}
},
bindWindowEvents() {
@ -3076,6 +3086,10 @@ function beforeUnload(evt) {
return false;
}
function webViewerAnnotationEditorStatesChanged(data) {
PDFViewerApplication.externalServices.updateEditorStates(data);
}
/* Abstract factory for the print service. */
const PDFPrintServiceFactory = {
instance: {

View File

@ -640,6 +640,10 @@ class BaseViewer {
if (this._scriptingManager) {
this._scriptingManager.setDocument(null);
}
if (this.#annotationEditorUIManager) {
this.#annotationEditorUIManager.destroy();
this.#annotationEditorUIManager = null;
}
}
this.pdfDocument = pdfDocument;
@ -899,7 +903,6 @@ class BaseViewer {
}
_resetView() {
this.#annotationEditorUIManager = null;
this._pages = [];
this._currentPageNumber = 1;
this._currentScale = UNKNOWN_SCALE;

View File

@ -271,6 +271,20 @@ class MozL10n {
window.addEventListener("save", handleEvent);
})();
(function listenEditingEvent() {
const handleEvent = function ({ detail }) {
if (!PDFViewerApplication.initialized) {
return;
}
PDFViewerApplication.eventBus.dispatch("editingaction", {
source: window,
name: detail.name,
});
};
window.addEventListener("editingaction", handleEvent);
})();
class FirefoxComDataRangeTransport extends PDFDataRangeTransport {
requestDataRange(begin, end) {
FirefoxCom.request("requestDataRange", { begin, end });
@ -384,6 +398,10 @@ class FirefoxExternalServices extends DefaultExternalServices {
return new FirefoxPreferences();
}
static updateEditorStates(data) {
FirefoxCom.request("updateEditorStates", data);
}
static createL10n(options) {
const mozL10n = document.mozL10n;
// TODO refactor mozL10n.setExternalLocalizerServices