Bug 1777695 - Add some entries in the context menu when editing a PDF r=mconley

It adds the entries Undo, Redo, Cut, Copy, Paste, Delete and Select all,
only when the pdf editing mode is enabled.

Differential Revision: https://phabricator.services.mozilla.com/D151122
This commit is contained in:
Calixte 2022-07-08 08:27:18 +00:00
Родитель a753b3db0b
Коммит 981db3428b
7 изменённых файлов: 562 добавлений и 3 удалений

Просмотреть файл

@ -893,6 +893,7 @@ class ContextMenuChild extends JSWindowActorChild {
context.onSpellcheckable = false;
context.onTextInput = false;
context.onVideo = false;
context.inPDFEditor = false;
// Remember the node and its owner document that was clicked
// This may be modifed before sending to nsContextMenu
@ -918,6 +919,10 @@ class ContextMenuChild extends JSWindowActorChild {
context.inPDFViewer =
context.target.ownerDocument.nodePrincipal.originNoSuffix ==
"resource://pdf.js";
if (context.inPDFViewer) {
context.pdfEditorStates = context.target.ownerDocument.editorStates;
context.inPDFEditor = !!context.pdfEditorStates?.isEditing;
}
// Check if we are in a synthetic document (stand alone image, video, etc.).
context.inSyntheticDoc = context.target.ownerDocument.mozSyntheticDocument;

Просмотреть файл

@ -310,6 +310,31 @@
data-l10n-id="main-context-menu-print-selection"
oncommand="gContextMenu.printSelection();"/>
<menuseparator id="context-sep-selectall"/>
<menuitem id="context-pdfjs-undo"
data-l10n-id="text-action-undo"
oncommand="gContextMenu.pdfJSCmd('undo');"/>
<menuitem id="context-pdfjs-redo"
data-l10n-id="text-action-redo"
oncommand="gContextMenu.pdfJSCmd('redo');"/>
<menuseparator id="context-sep-pdfjs-redo"/>
<menuitem id="context-pdfjs-cut"
data-l10n-id="text-action-cut"
oncommand="gContextMenu.pdfJSCmd('cut');"/>
<menuitem id="context-pdfjs-copy"
data-l10n-id="text-action-copy"
oncommand="gContextMenu.pdfJSCmd('copy');"/>
<menuitem id="context-pdfjs-paste"
data-l10n-id="text-action-paste"
oncommand="gContextMenu.pdfJSCmd('paste');"/>
<menuitem id="context-pdfjs-delete"
data-l10n-id="text-action-delete"
oncommand="gContextMenu.pdfJSCmd('delete');"/>
<menuitem id="context-pdfjs-selectall"
data-l10n-id="text-action-select-all"
oncommand="gContextMenu.pdfJSCmd('selectAll');"/>
<menuseparator id="context-sep-pdfjs-selectall"/>
<menuitem id="context-take-screenshot"
data-l10n-id="main-context-menu-take-screenshot"
oncommand="gContextMenu.takeScreenshot();"/>

Просмотреть файл

@ -193,6 +193,7 @@ class nsContextMenu {
this.isDesignMode = context.isDesignMode;
this.inFrame = context.inFrame;
this.inPDFViewer = context.inPDFViewer;
this.inPDFEditor = context.inPDFEditor;
this.inSrcdocFrame = context.inSrcdocFrame;
this.inSyntheticDoc = context.inSyntheticDoc;
this.inTabBrowser = context.inTabBrowser;
@ -226,6 +227,8 @@ class nsContextMenu {
this.onTextInput = context.onTextInput;
this.onVideo = context.onVideo;
this.pdfEditorStates = context.pdfEditorStates;
this.target = context.target;
this.targetIdentifier = context.targetIdentifier;
@ -338,6 +341,7 @@ class nsContextMenu {
this.initViewSourceItems();
this.initScreenshotItem();
this.initPasswordControlItems();
this.initPDFItems();
this.showHideSeparators(aXulMenu);
if (!aXulMenu.showHideSeparators) {
@ -350,6 +354,60 @@ class nsContextMenu {
}
}
initPDFItems() {
for (const id of [
"context-pdfjs-undo",
"context-pdfjs-redo",
"context-sep-pdfjs-redo",
"context-pdfjs-cut",
"context-pdfjs-copy",
"context-pdfjs-paste",
"context-pdfjs-delete",
"context-pdfjs-selectall",
"context-sep-pdfjs-selectall",
]) {
this.showItem(id, this.inPDFEditor);
}
if (!this.inPDFEditor) {
return;
}
const {
isEmpty,
hasEmptyClipboard,
hasSomethingToUndo,
hasSomethingToRedo,
hasSelectedEditor,
} = this.pdfEditorStates;
this.setItemAttr("context-pdfjs-undo", "disabled", !hasSomethingToUndo);
this.setItemAttr("context-pdfjs-redo", "disabled", !hasSomethingToRedo);
this.setItemAttr(
"context-sep-pdfjs-redo",
"disabled",
!hasSomethingToUndo && !hasSomethingToRedo
);
this.setItemAttr(
"context-pdfjs-cut",
"disabled",
isEmpty || !hasSelectedEditor
);
this.setItemAttr(
"context-pdfjs-copy",
"disabled",
isEmpty || !hasSelectedEditor
);
this.setItemAttr("context-pdfjs-paste", "disabled", hasEmptyClipboard);
this.setItemAttr(
"context-pdfjs-delete",
"disabled",
isEmpty || !hasSelectedEditor
);
this.setItemAttr("context-pdfjs-selectall", "disabled", isEmpty);
this.setItemAttr("context-sep-pdfjs-selectall", "disabled", isEmpty);
}
initOpenItems() {
var isMailtoInternal = false;
if (this.onMailtoLink) {
@ -551,7 +609,9 @@ class nsContextMenu {
// (or is in a frame), or a canvas. If this isn't an image, check
// if there is a background image.
let showViewImage =
(this.onImage && (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas;
((this.onImage && (!this.inSyntheticDoc || this.inFrame)) ||
this.onCanvas) &&
!this.inPDFViewer;
let showBGImage =
this.hasBGImage &&
!this.hasMultipleBGImages &&
@ -567,7 +627,10 @@ class nsContextMenu {
this.showItem("context-viewimage", showViewImage || showBGImage);
// Save image depends on having loaded its content.
this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
this.showItem(
"context-saveimage",
(this.onLoadedImage || this.onCanvas) && !this.inPDFEditor
);
// Copy image contents depends on whether we're on an image.
// Note: the element doesn't exist on all platforms, but showItem() takes
@ -879,7 +942,8 @@ class nsContextMenu {
this.onImage ||
this.onVideo ||
this.onAudio ||
this.inSyntheticDoc
this.inSyntheticDoc ||
this.inPDFEditor
) || this.isDesignMode
);
@ -1402,6 +1466,10 @@ class nsContextMenu {
}
}
pdfJSCmd(name) {
this.browser.sendMessageToActor("PDFJS:Editing", { name }, "Pdfjs");
}
// View Partial Source
viewPartialSource() {
let { browser } = this;

Просмотреть файл

@ -605,6 +605,31 @@ class ChromeActions {
}
return result;
}
/**
* Set the different editor states in order to be able to update the context
* menu.
* @param {Object} details
*/
updateEditorStates({ details }) {
const doc = this.domWindow.document;
if (!doc.editorStates) {
doc.editorStates = {
isEditing: false,
isEmpty: true,
hasEmptyClipboard: true,
hasSomethingToUndo: false,
hasSomethingToRedo: false,
hasSelectedEditor: false,
};
}
const { editorStates } = doc;
for (const [key, value] of Object.entries(details)) {
if (typeof value === "boolean" && key in editorStates) {
editorStates[key] = value;
}
}
}
}
/**

Просмотреть файл

@ -39,6 +39,10 @@ class PdfjsChild extends JSWindowActorChild {
break;
}
case "PDFJS:Editing":
let data = Cu.cloneInto(msg.data, this.contentWindow);
this.dispatchEvent("editingaction", data);
break;
case "PDFJS:ZoomIn":
case "PDFJS:ZoomOut":
case "PDFJS:ZoomReset":

Просмотреть файл

@ -40,3 +40,4 @@ support-files =
[browser_pdfjs_views.js]
[browser_pdfjs_zoom.js]
skip-if = (verify && debug && (os == 'win'))
[browser_pdfjs_editing_contextmenu.js]

Просмотреть файл

@ -0,0 +1,431 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const RELATIVE_DIR = "toolkit/components/pdfjs/test/";
const TESTROOT = "http://example.com/browser/" + RELATIVE_DIR;
// This is a modified version from browser_contextmenuFillLogins.js.
async function openContextMenuAt(browser, x, y) {
const doc = browser.ownerDocument;
const contextMenu = doc.getElementById("contentAreaContextMenu");
const contextMenuShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
);
// Synthesize a contextmenu event to actually open the context menu.
await BrowserTestUtils.synthesizeMouseAtPoint(
x,
y,
{
type: "contextmenu",
button: 2,
},
browser
);
await contextMenuShownPromise;
return contextMenu;
}
/**
* The text layer contains some spans with the text of the pdf.
* @param {Object} browser
* @param {string} text
* @returns {Object} the bbox of the span containing the text.
*/
async function getSpanBox(browser, text) {
return SpecialPowers.spawn(browser, [text], async function(text) {
const { ContentTaskUtils } = ChromeUtils.import(
"resource://testing-common/ContentTaskUtils.jsm"
);
const { document } = content;
await ContentTaskUtils.waitForCondition(
() => !!document.querySelector(".textLayer .endOfContent"),
"The text layer must be displayed"
);
let targetSpan = null;
for (const span of document.querySelectorAll(
`.textLayer span[role="presentation"]`
)) {
if (span.innerText.includes(text)) {
targetSpan = span;
break;
}
}
Assert.ok(targetSpan, `document must have a span containing '${text}'`);
const { x, y, width, height } = targetSpan.getBoundingClientRect();
return { x, y, width, height };
});
}
/**
* Open a context menu and get the pdfjs entries
* @param {Object} browser
* @param {Object} box
* @returns {Map<string,HTMLElement>} the pdfjs menu entries.
*/
async function getContextMenuItems(browser, box) {
const { x, y, width, height } = box;
const menuitems = [
"context-pdfjs-undo",
"context-pdfjs-redo",
"context-sep-pdfjs-redo",
"context-pdfjs-cut",
"context-pdfjs-copy",
"context-pdfjs-paste",
"context-pdfjs-delete",
"context-pdfjs-selectall",
"context-sep-pdfjs-selectall",
];
await openContextMenuAt(browser, x + width / 2, y + height / 2);
const results = new Map();
const doc = browser.ownerDocument;
for (const menuitem of menuitems) {
const item = doc.getElementById(menuitem);
results.set(menuitem, item || null);
}
return results;
}
/**
* Open a context menu on the element corresponding to the given selector
* and returs the pdfjs menu entries.
* @param {Object} browser
* @param {string} selector
* @returns {Map<string,HTMLElement>} the pdfjs menu entries.
*/
async function getContextMenuItemsOn(browser, selector) {
const box = await SpecialPowers.spawn(browser, [selector], async function(
selector
) {
const element = content.document.querySelector(selector);
const { x, y, width, height } = element.getBoundingClientRect();
return { x, y, width, height };
});
return getContextMenuItems(browser, box);
}
/**
* Hide the context menu.
* @param {Object} browser
*/
async function hideContextMenu(browser) {
const doc = browser.ownerDocument;
const contextMenu = doc.getElementById("contentAreaContextMenu");
const popupHiddenPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popuphidden"
);
contextMenu.hidePopup();
await popupHiddenPromise;
}
/**
* Enable an editor (Ink, FreeText, ...).
* @param {Object} browser
* @param {string} name
*/
async function enableEditor(browser, name) {
await SpecialPowers.spawn(browser, [name], async function(name) {
const button = content.document.querySelector(`#editor${name}`);
button.click();
});
}
/**
* Click at the given coordinates.
* @param {Object} browser
* @param {number} x
* @param {number} y
*/
async function clickAt(browser, x, y) {
BrowserTestUtils.synthesizeMouseAtPoint(
x,
y,
{
type: "mousedown",
button: 0,
},
browser
);
BrowserTestUtils.synthesizeMouseAtPoint(
x,
y,
{
type: "mouseup",
button: 0,
},
browser
);
}
/**
* Click on the element corresponding to the given selector.
* @param {Object} browser
* @param {string} selector
*/
async function clickOn(browser, selector) {
const [x, y] = await SpecialPowers.spawn(browser, [selector], async function(
selector
) {
const element = content.document.querySelector(selector);
const { x, y, width, height } = element.getBoundingClientRect();
return [x + width / 2, y + height / 2];
});
await clickAt(browser, x, y);
}
/**
* Write some text using the keyboard.
* @param {Object} browser
* @param {string} text
*/
async function write(browser, text) {
await SpecialPowers.spawn(browser, [text], async function(text) {
const { ContentTaskUtils } = ChromeUtils.import(
"resource://testing-common/ContentTaskUtils.jsm"
);
const EventUtils = ContentTaskUtils.getEventUtils(content);
for (const char of text.split("")) {
EventUtils.synthesizeKey(char, {}, content);
}
});
}
/**
* Add a FreeText annotation and write some text inside.
* @param {Object} browser
* @param {string} text
* @param {Object} box
*/
async function addFreeText(browser, text, box) {
const { x, y, width, height } = box;
await clickAt(browser, x + 0.1 * width, y + 0.5 * height);
await write(browser, text);
await clickAt(browser, x + 0.1 * width, y + 2 * height);
}
/**
* Count the number of elements corresponding to the given selector.
* @param {Object} browser
* @param {string} selector
* @returns
*/
async function countElements(browser, selector) {
return SpecialPowers.spawn(browser, [selector], async function(selector) {
const { document } = content;
return document.querySelectorAll(selector).length;
});
}
/**
* Asserts that the enabled pdfjs menuitems are the expected ones.
* @param {Map<string,HTMLElement>} menuitems
* @param {Array<string>} expected
*/
function assertMenuitems(menuitems, expected) {
Assert.deepEqual(
[...menuitems.values()]
.filter(
elmt =>
!elmt.id.includes("-sep-") &&
!elmt.hidden &&
elmt.getAttribute("disabled") === "false"
)
.map(elmt => elmt.id),
expected
);
}
// Text copy, paste, undo, redo, delete and select all in using the context
// menu.
add_task(async function test() {
let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
let handlerInfo = mimeService.getFromTypeAndExtension(
"application/pdf",
"pdf"
);
// Make sure pdf.js is the default handler.
is(
handlerInfo.alwaysAskBeforeHandling,
false,
"pdf handler defaults to always-ask is false"
);
is(
handlerInfo.preferredAction,
Ci.nsIHandlerInfo.handleInternally,
"pdf handler defaults to internal"
);
info("Pref action: " + handlerInfo.preferredAction);
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:blank" },
async function(browser) {
await SpecialPowers.pushPrefEnv({
set: [["pdfjs.annotationEditorMode", 0]],
});
// check that PDF is opened with internal viewer
await waitForPdfJS(browser, TESTROOT + "file_pdfjs_test.pdf");
const spanBox = await getSpanBox(browser, "and found references");
let menuitems = await getContextMenuItems(browser, spanBox);
// Nothing have been edited, hence the context menu doesn't contain any
// pdf entries.
Assert.ok(
[...menuitems.values()].every(elmt => elmt.hidden),
"No visible pdf menuitem"
);
await hideContextMenu(browser);
await enableEditor(browser, "FreeText");
await addFreeText(browser, "hello", spanBox);
Assert.equal(await countElements(browser, ".freeTextEditor"), 1);
menuitems = await getContextMenuItems(browser, spanBox);
assertMenuitems(menuitems, [
"context-pdfjs-undo", // Last created editor is undoable
"context-pdfjs-selectall", // and selectable.
]);
// Undo.
menuitems.get("context-pdfjs-undo").click();
await hideContextMenu(browser);
Assert.equal(
await countElements(browser, ".freeTextEditor"),
0,
"The FreeText editor must have been removed"
);
menuitems = await getContextMenuItems(browser, spanBox);
// The editor removed thanks to "undo" is now redoable
assertMenuitems(menuitems, ["context-pdfjs-redo"]);
menuitems.get("context-pdfjs-redo").click();
await hideContextMenu(browser);
Assert.equal(
await countElements(browser, ".freeTextEditor"),
1,
"The FreeText editor must have been added back"
);
await clickOn(browser, "#pdfjs_internal_editor_0");
menuitems = await getContextMenuItemsOn(
browser,
"#pdfjs_internal_editor_0"
);
assertMenuitems(menuitems, [
"context-pdfjs-undo",
"context-pdfjs-cut",
"context-pdfjs-copy",
"context-pdfjs-delete",
"context-pdfjs-selectall",
]);
menuitems.get("context-pdfjs-cut").click();
await hideContextMenu(browser);
Assert.equal(
await countElements(browser, ".freeTextEditor"),
0,
"The FreeText editor must have been cut"
);
menuitems = await getContextMenuItems(browser, spanBox);
assertMenuitems(menuitems, ["context-pdfjs-undo", "context-pdfjs-paste"]);
menuitems.get("context-pdfjs-paste").click();
await hideContextMenu(browser);
Assert.equal(
await countElements(browser, ".freeTextEditor"),
1,
"The FreeText editor must have been pasted"
);
await clickOn(browser, "#pdfjs_internal_editor_2");
menuitems = await getContextMenuItemsOn(
browser,
"#pdfjs_internal_editor_2"
);
assertMenuitems(menuitems, [
"context-pdfjs-undo",
"context-pdfjs-cut",
"context-pdfjs-copy",
"context-pdfjs-paste",
"context-pdfjs-delete",
"context-pdfjs-selectall",
]);
menuitems.get("context-pdfjs-delete").click();
await hideContextMenu(browser);
Assert.equal(
await countElements(browser, ".freeTextEditor"),
0,
"The FreeText editor must have been deleted"
);
menuitems = await getContextMenuItems(browser, spanBox);
menuitems.get("context-pdfjs-paste").click();
await hideContextMenu(browser);
Assert.equal(
await countElements(browser, ".freeTextEditor"),
1,
"The FreeText editor must have been pasted"
);
await clickOn(browser, "#pdfjs_internal_editor_3");
menuitems = await getContextMenuItemsOn(
browser,
"#pdfjs_internal_editor_3"
);
menuitems.get("context-pdfjs-copy").click();
menuitems.get("context-pdfjs-paste").click();
await hideContextMenu(browser);
Assert.equal(
await countElements(browser, ".freeTextEditor"),
2,
"The FreeText editor must have been pasted"
);
menuitems = await getContextMenuItems(browser, spanBox);
menuitems.get("context-pdfjs-selectall").click();
menuitems.get("context-pdfjs-delete").click();
await hideContextMenu(browser);
Assert.equal(
await countElements(browser, ".freeTextEditor"),
0,
"All the FreeText editors must have been deleted"
);
await SpecialPowers.spawn(browser, [], async function() {
var viewer = content.wrappedJSObject.PDFViewerApplication;
await viewer.close();
});
}
);
});