Bug 1058712, e10s, support for copy image command, r=ehsan,mconley
authorNeil Deakin <neil@mozilla.com>
Tue, 21 Apr 2015 20:09:14 -0400
changeset 271810 9b75aac198d0480ed6171099d086d37da97f29ff
parent 271809 c36ce7aed6d9eb43e3b4962afe1cb961087ed558
child 271811 f67a0b28268035b608766a7e6a69a0b4659d4dc0
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, mconley
bugs1058712
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1058712, e10s, support for copy image command, r=ehsan,mconley
browser/base/content/content.js
browser/base/content/nsContextMenu.js
browser/base/content/test/general/browser_clipboard.js
docshell/base/nsIContentViewerEdit.idl
layout/base/nsDocumentViewer.cpp
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -118,16 +118,23 @@ let handleContentContextMenu = function 
     let editFlags = SpellCheckHelper.isEditable(event.target, content);
     let spellInfo;
     if (editFlags &
         (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
       spellInfo =
         InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
     }
 
+    // Set the event target first as the copy image command needs it to
+    // determine what was context-clicked on. Then, update the state of the
+    // commands on the context menu.
+    docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
+            .setCommandNode(event.target);
+    event.target.ownerDocument.defaultView.updateCommands("contentcontextmenu");
+
     let customMenuItems = PageMenuChild.build(event.target);
     let principal = doc.nodePrincipal;
     sendSyncMessage("contextmenu",
                     { editFlags, spellInfo, customMenuItems, addonInfo,
                       principal, docLocation, charSet, baseURI, referrer,
                       referrerPolicy, contentType, contentDisposition,
                       frameOuterWindowID },
                     { event, popupNode: event.target });
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -623,17 +623,19 @@ nsContextMenu.prototype = {
                                         .outerWindowID;
     }
     this.onSocial = !!this.browser.getAttribute("origin");
 
     // Check if we are in a synthetic document (stand alone image, video, etc.).
     this.inSyntheticDoc = ownerDoc.mozSyntheticDocument;
     // First, do checks for nodes that never have children.
     if (this.target.nodeType == Node.ELEMENT_NODE) {
-      // See if the user clicked on an image.
+      // See if the user clicked on an image. This check mirrors
+      // nsDocumentViewer::GetInImage. Make sure to update both if this is
+      // changed.
       if (this.target instanceof Ci.nsIImageLoadingContent &&
           this.target.currentURI) {
         this.onImage = true;
 
         var request =
           this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
         if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
           this.onLoadedImage = true;
--- a/browser/base/content/test/general/browser_clipboard.js
+++ b/browser/base/content/test/general/browser_clipboard.js
@@ -1,34 +1,38 @@
 // This test is used to check copy and paste in editable areas to ensure that non-text
 // types (html and images) are copied to and pasted from the clipboard properly.
 
-let testPage = "<div id='main' contenteditable='true'>Test <b>Bold</b> After Text</div>";
+let testPage = "<body style='margin: 0'><img id='img' tabindex='1' src='http://example.org/browser/browser/base/content/test/general/moz.png'>" +
+               "  <div id='main' contenteditable='true'>Test <b>Bold</b> After Text</div>" +
+               "</body>";
 
 add_task(function*() {
   let tab = gBrowser.addTab();
   let browser = gBrowser.getBrowserForTab(tab);
 
   gBrowser.selectedTab = tab;
 
   yield promiseTabLoadEvent(tab, "data:text/html," + escape(testPage));
   yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW);
 
-  let results = yield ContentTask.spawn(browser, {}, function* () {
+  const modifier = (content.navigator.platform.indexOf("Mac") >= 0) ?
+                   Components.interfaces.nsIDOMWindowUtils.MODIFIER_META :
+                   Components.interfaces.nsIDOMWindowUtils.MODIFIER_CONTROL;
+
+  let results = yield ContentTask.spawn(browser, { modifier: modifier },
+                                        function* (arg) {
     var doc = content.document;
     var main = doc.getElementById("main");
     main.focus();
 
     const utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                          .getInterface(Components.interfaces.nsIDOMWindowUtils);
 
-    const modifier = (content.navigator.platform.indexOf("Mac") >= 0) ?
-                     Components.interfaces.nsIDOMWindowUtils.MODIFIER_META :
-                     Components.interfaces.nsIDOMWindowUtils.MODIFIER_CONTROL;
-
+    const modifier = arg.modifier;
     function sendKey(key)
     {
      if (utils.sendKeyEvent("keydown", key, 0, modifier)) {
        utils.sendKeyEvent("keypress", key, key.charCodeAt(0), modifier);
      }
      utils.sendKeyEvent("keyup", key, 0, modifier);
     }
 
@@ -41,30 +45,30 @@ add_task(function*() {
     let selection = doc.getSelection();
     selection.modify("move", "left", "line");
     selection.modify("move", "right", "character");
     selection.modify("move", "right", "character");
     selection.modify("move", "right", "character");
     selection.modify("extend", "right", "word");
     selection.modify("extend", "right", "word");
 
-    yield new content.Promise((resolve, reject) => {
+    yield new Promise((resolve, reject) => {
       addEventListener("copy", function copyEvent(event) {
         removeEventListener("copy", copyEvent, true);
         // The data is empty as the selection is copied during the event default phase.
         is(event.clipboardData.mozItemCount, 0, "Zero items on clipboard");
         resolve();
       }, true)
 
       sendKey("c");
     });
 
     selection.modify("move", "right", "line");
 
-    yield new content.Promise((resolve, reject) => {
+    yield new Promise((resolve, reject) => {
       addEventListener("paste", function copyEvent(event) {
         removeEventListener("paste", copyEvent, true);
         let clipboardData = event.clipboardData; 
         is(clipboardData.mozItemCount, 1, "One item on clipboard");
         is(clipboardData.types.length, 2, "Two types on clipboard");
         is(clipboardData.types[0], "text/html", "text/html on clipboard");
         is(clipboardData.types[1], "text/plain", "text/plain on clipboard");
         is(clipboardData.getData("text/html"), "t <b>Bold</b>", "text/html value");
@@ -75,49 +79,105 @@ add_task(function*() {
     });
 
     is(main.innerHTML, "Test <b>Bold</b> After Textt <b>Bold</b>", "Copy and paste html");
 
     selection.modify("extend", "left", "word");
     selection.modify("extend", "left", "word");
     selection.modify("extend", "left", "character");
 
-    yield new content.Promise((resolve, reject) => {
+    yield new Promise((resolve, reject) => {
       addEventListener("cut", function copyEvent(event) {
         removeEventListener("cut", copyEvent, true);
         event.clipboardData.setData("text/plain", "Some text");
         event.clipboardData.setData("text/html", "<i>Italic</i> ");
         selection.deleteFromDocument();
         event.preventDefault();
         resolve();
       }, true)
       sendKey("x");
     });
 
     selection.modify("move", "left", "line");
 
-    yield new content.Promise((resolve, reject) => {
+    yield new Promise((resolve, reject) => {
       addEventListener("paste", function copyEvent(event) {
         removeEventListener("paste", copyEvent, true);
         let clipboardData = event.clipboardData; 
         is(clipboardData.mozItemCount, 1, "One item on clipboard 2");
         is(clipboardData.types.length, 2, "Two types on clipboard 2");
         is(clipboardData.types[0], "text/html", "text/html on clipboard 2");
         is(clipboardData.types[1], "text/plain", "text/plain on clipboard 2");
         is(clipboardData.getData("text/html"), "<i>Italic</i> ", "text/html value 2");
         is(clipboardData.getData("text/plain"), "Some text", "text/plain value 2");
         resolve();
       }, true)
       sendKey("v");
     });
 
     is(main.innerHTML, "<i>Italic</i> Test <b>Bold</b> After<b></b>", "Copy and paste html 2");
+
     return results;
   });
 
   is(results.length, 15, "Correct number of results");
   for (var t = 0; t < results.length; t++) {
     ok(results[t].startsWith("PASSED"), results[t]);
   }
 
+  // Next, check that the Copy Image command works.
+
+  // The context menu needs to be opened to properly initialize for the copy
+  // image command to run.
+  let contextMenu = document.getElementById("contentAreaContextMenu");
+  let contextMenuShown = promisePopupShown(contextMenu);
+  BrowserTestUtils.synthesizeMouseAtCenter("#img", { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
+  yield contextMenuShown;
+
+  document.getElementById("context-copyimage-contents").doCommand();
+
+  contextMenu.hidePopup();
+  yield promisePopupHidden(contextMenu);
+
+  // Focus the content again
+  yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW);
+
+  let expectedContent = yield ContentTask.spawn(browser, { modifier: modifier },
+                                                function* (arg) {
+    var doc = content.document;
+    var main = doc.getElementById("main");
+    main.focus();
+
+    yield new Promise((resolve, reject) => {
+      addEventListener("paste", function copyEvent(event) {
+        removeEventListener("paste", copyEvent, true);
+        let clipboardData = event.clipboardData;
+
+        // DataTransfer doesn't support the image types yet, so only text/html
+        // will be present.
+        if (clipboardData.getData("text/html") !=
+            '<img id="img" tabindex="1" src="http://example.org/browser/browser/base/content/test/general/moz.png">') {
+          reject();
+        }
+        resolve();
+      }, true)
+
+      const utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                           .getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+      const modifier = arg.modifier;
+      if (utils.sendKeyEvent("keydown", "v", 0, modifier)) {
+        utils.sendKeyEvent("keypress", "v", "v".charCodeAt(0), modifier);
+      }
+      utils.sendKeyEvent("keyup", "v", 0, modifier);
+    });
+
+    // Return the new content which should now include an image.
+    return main.innerHTML;
+  });
+
+  is(expectedContent, '<i>Italic</i> <img id="img" tabindex="1" ' +
+                      'src="http://example.org/browser/browser/base/content/test/general/moz.png">' +
+                      'Test <b>Bold</b> After<b></b>', "Paste after copy image");
+
   gBrowser.removeCurrentTab();
 });
 
--- a/docshell/base/nsIContentViewerEdit.idl
+++ b/docshell/base/nsIContentViewerEdit.idl
@@ -1,17 +1,19 @@
 /* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  *
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
-[scriptable, uuid(AF13EA3A-D488-4308-B843-526E055AB943)]
+interface nsIDOMNode;
+
+[scriptable, uuid(35BE2D7E-F29B-48EC-BF7E-80A30A724DE3)]
 interface nsIContentViewerEdit : nsISupports
 {
 	void clearSelection();
 	void selectAll();
 
 	void copySelection();
 	readonly attribute boolean copyable;
 
@@ -22,9 +24,13 @@ interface nsIContentViewerEdit : nsISupp
 	const long COPY_IMAGE_HTML = 0x0002;
 	const long COPY_IMAGE_DATA = 0x0004;
 	const long COPY_IMAGE_ALL = -1;
 	void copyImage(in long aCopyFlags);
 	readonly attribute boolean inImage;
 
 	AString getContents(in string aMimeType, in boolean aSelectionOnly);
 	readonly attribute boolean canGetContents;
+
+	// Set the node that will be the subject of the editing commands above.
+	// Usually this will be the node that was context-clicked.
+	void setCommandNode(in nsIDOMNode aNode);
 };
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -55,16 +55,17 @@
 #include "nsDocShell.h"
 #include "nsIBaseWindow.h"
 #include "nsILayoutHistoryState.h"
 #include "nsCharsetSource.h"
 #include "nsHTMLReflowState.h"
 #include "nsIImageLoadingContent.h"
 #include "nsCopySupport.h"
 #include "nsIDOMHTMLFrameSetElement.h"
+#include "nsIDOMHTMLImageElement.h"
 #ifdef MOZ_XUL
 #include "nsIXULDocument.h"
 #include "nsXULPopupManager.h"
 #endif
 
 #include "nsIClipboardHelper.h"
 
 #include "nsPIDOMWindow.h"
@@ -2702,16 +2703,30 @@ NS_IMETHODIMP nsDocumentViewer::GetCanGe
 {
   NS_ENSURE_ARG_POINTER(aCanGetContents);
   *aCanGetContents = false;
   NS_ENSURE_STATE(mDocument);
   *aCanGetContents = nsCopySupport::CanCopy(mDocument);
   return NS_OK;
 }
 
+NS_IMETHODIMP nsDocumentViewer::SetCommandNode(nsIDOMNode* aNode)
+{
+  nsIDocument* document = GetDocument();
+  NS_ENSURE_STATE(document);
+
+  nsCOMPtr<nsPIDOMWindow> window(document->GetWindow());
+  NS_ENSURE_TRUE(window, NS_ERROR_NOT_AVAILABLE);
+
+  nsCOMPtr<nsPIWindowRoot> root = window->GetTopWindowRoot();
+  NS_ENSURE_STATE(root);
+
+  root->SetPopupNode(aNode);
+  return NS_OK;
+}
 
 /* ========================================================================================
  * nsIContentViewerFile
  * ======================================================================================== */
 /** ---------------------------------------------------
  *  See documentation above in the nsIContentViewerfile class definition
  *	@update 01/24/00 dwc
  */
@@ -3524,18 +3539,26 @@ NS_IMETHODIMP nsDocumentViewer::GetInIma
   *aInImage = false;
 
   // get the popup image
   nsCOMPtr<nsIImageLoadingContent> node;
   nsresult rv = GetPopupImageNode(getter_AddRefs(node));
   if (NS_FAILED(rv)) return rv;
   NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
 
-  // if we made it here, we're in an image
-  *aInImage = true;
+  // Make sure there is a URI assigned. This allows <input type="image"> to
+  // be an image but rejects other <input> types. This matches what
+  // nsContextMenu.js does.
+  nsCOMPtr<nsIURI> uri;
+  node->GetCurrentURI(getter_AddRefs(uri));
+  if (uri) {
+    // if we made it here, we're in an image
+    *aInImage = true;
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP nsDocViewerSelectionListener::NotifySelectionChanged(nsIDOMDocument *, nsISelection *, int16_t aReason)
 {
   NS_ASSERTION(mDocViewer, "Should have doc viewer!");
 
   // get the selection state