Bug 1134769, move selection info retrieval on context menu to content process, r=mconley
authorNeil Deakin <neil@mozilla.com>
Wed, 29 Apr 2015 08:38:42 -0400
changeset 241582 032a7f96419355b82de528bc43609f6063a76de3
parent 241581 5ce83454a0e4b2806e44d06f8fd9ad52f5ba52f6
child 241583 94238545f5c1926fede75391a3f6a17cda3c62ee
push id28665
push userkwierso@gmail.com
push dateWed, 29 Apr 2015 23:43:43 +0000
treeherdermozilla-central@a86ed85747d8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1134769
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 1134769, move selection info retrieval on context menu to content process, r=mconley
browser/base/content/browser.js
browser/base/content/content.js
browser/base/content/nsContextMenu.js
browser/base/content/tabbrowser.xml
toolkit/content/browser-child.js
toolkit/content/widgets/browser.xml
toolkit/content/widgets/remote-browser.xml
toolkit/modules/BrowserUtils.jsm
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5273,61 +5273,27 @@ function UpdateDynamicShortcutTooltipTex
         args.push(ShortcutUtils.prettifyShortcut(shortcut));
       }
     }
     gDynamicTooltipCache.set(nodeId, gNavigatorBundle.getFormattedString(strId, args));
   }
   aTooltip.setAttribute("label", gDynamicTooltipCache.get(nodeId));
 }
 
-/**
- * Gets the selected text in the active browser. Leading and trailing
- * whitespace is removed, and consecutive whitespace is replaced by a single
- * space. A maximum of 150 characters will be returned, regardless of the value
- * of aCharLen.
- *
- * @param aCharLen
- *        The maximum number of characters to return.
- */
 function getBrowserSelection(aCharLen) {
-  // selections of more than 150 characters aren't useful
-  const kMaxSelectionLen = 150;
-  const charLen = Math.min(aCharLen || kMaxSelectionLen, kMaxSelectionLen);
-
-  let [element, focusedWindow] = BrowserUtils.getFocusSync(document);
-  var selection = focusedWindow.getSelection().toString();
-  // try getting a selected text in text input.
-  if (!selection) {
-    var isOnTextInput = function isOnTextInput(elem) {
-      // we avoid to return a value if a selection is in password field.
-      // ref. bug 565717
-      return elem instanceof HTMLTextAreaElement ||
-             (elem instanceof HTMLInputElement && elem.mozIsTextField(true));
-    };
-
-    if (isOnTextInput(element)) {
-      selection = element.QueryInterface(Ci.nsIDOMNSEditableElement)
-                         .editor.selection.toString();
-    }
-  }
-
-  if (selection) {
-    if (selection.length > charLen) {
-      // only use the first charLen important chars. see bug 221361
-      var pattern = new RegExp("^(?:\\s*.){0," + charLen + "}");
-      pattern.test(selection);
-      selection = RegExp.lastMatch;
-    }
-
-    selection = selection.trim().replace(/\s+/g, " ");
-
-    if (selection.length > charLen)
-      selection = selection.substr(0, charLen);
-  }
-  return selection;
+  Deprecated.warning("getBrowserSelection",
+                     "https://bugzilla.mozilla.org/show_bug.cgi?id=1134769");
+
+  let focusedElement = document.activeElement;
+  if (focusedElement && focusedElement.localName == "browser" &&
+      focusedElement.isRemoteBrowser) {
+    throw "getBrowserSelection doesn't support child process windows";
+  }
+
+  return BrowserUtils.getSelectionDetails(window, aCharLen).text;
 }
 
 var gWebPanelURI;
 function openWebPanel(title, uri) {
   // Ensure that the web panels sidebar is open.
   SidebarUI.show("viewWebPanelsSidebar");
 
   // Set the title of the panel.
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -109,16 +109,18 @@ let handleContentContextMenu = function 
         contentType = props.get("type", Ci.nsISupportsCString).data;
         contentDisposition = props.get("content-disposition", Ci.nsISupportsCString).data;
       }
     } catch (e) {
       Cu.reportError(e);
     }
   }
 
+  let selectionInfo = BrowserUtils.getSelectionDetails(content);
+
   if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
     let editFlags = SpellCheckHelper.isEditable(event.target, content);
     let spellInfo;
     if (editFlags &
         (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
       spellInfo =
         InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
     }
@@ -131,17 +133,17 @@ let handleContentContextMenu = function 
     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 },
+                      frameOuterWindowID, selectionInfo },
                     { event, popupNode: event.target });
   }
   else {
     // Break out to the parent window and pass the add-on info along
     let browser = docShell.chromeEventHandler;
     let mainWin = browser.ownerDocument.defaultView;
     mainWin.gContextMenuContentData = {
       isRemote: false,
@@ -151,16 +153,17 @@ let handleContentContextMenu = function 
       addonInfo: addonInfo,
       documentURIObject: doc.documentURIObject,
       docLocation: docLocation,
       charSet: charSet,
       referrer: referrer,
       referrerPolicy: referrerPolicy,
       contentType: contentType,
       contentDisposition: contentDisposition,
+      selectionInfo: selectionInfo,
     };
   }
 }
 
 Cc["@mozilla.org/eventlistenerservice;1"]
   .getService(Ci.nsIEventListenerService)
   .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
 
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -37,17 +37,17 @@ nsContextMenu.prototype = {
 
     this.isFrameImage = document.getElementById("isFrameImage");
     this.ellipsis = "\u2026";
     try {
       this.ellipsis = gPrefService.getComplexValue("intl.ellipsis",
                                                    Ci.nsIPrefLocalizedString).data;
     } catch (e) { }
 
-    this.isContentSelected = this.isContentSelection();
+    this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
     this.onPlainTextLink = false;
 
     // Initialize (disable/remove) menu items.
     this.initItems();
 
     // Register this opening of the menu with telemetry:
     this._checkTelemetryForMenu(aXulMenu);
   },
@@ -88,77 +88,25 @@ nsContextMenu.prototype = {
       var mailtoHandler = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
                           getService(Ci.nsIExternalProtocolService).
                           getProtocolHandlerInfo("mailto");
       isMailtoInternal = (!mailtoHandler.alwaysAskBeforeHandling &&
                           mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
                           (mailtoHandler.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp));
     }
 
-    // Time to do some bad things and see if we've highlighted a URL that
-    // isn't actually linked.
-    if (this.isTextSelected && !this.onLink) {
-      // Ok, we have some text, let's figure out if it looks like a URL.
-      let selection =  this.focusedWindow.getSelection();
-      let linkText = selection.toString().trim();
-      let uri;
-      if (/^(?:https?|ftp):/i.test(linkText)) {
-        try {
-          uri = makeURI(linkText);
-        } catch (ex) {}
-      }
-      // Check if this could be a valid url, just missing the protocol.
-      else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) {
-        // Now let's see if this is an intentional link selection. Our guess is
-        // based on whether the selection begins/ends with whitespace or is
-        // preceded/followed by a non-word character.
-
-        // selection.toString() trims trailing whitespace, so we look for
-        // that explicitly in the first and last ranges.
-        let beginRange = selection.getRangeAt(0);
-        let delimitedAtStart = /^\s/.test(beginRange);
-        if (!delimitedAtStart) {
-          let container = beginRange.startContainer;
-          let offset = beginRange.startOffset;
-          if (container.nodeType == Node.TEXT_NODE && offset > 0)
-            delimitedAtStart = /\W/.test(container.textContent[offset - 1]);
-          else
-            delimitedAtStart = true;
-        }
+    if (this.isTextSelected && !this.onLink &&
+        this.selectionInfo && this.selectionInfo.linkURL) {
+      this.linkURL = this.selectionInfo.linkURL;
+      try {
+        this.linkURI = makeURI(this.linkURL);
+      } catch (ex) {}
 
-        let delimitedAtEnd = false;
-        if (delimitedAtStart) {
-          let endRange = selection.getRangeAt(selection.rangeCount - 1);
-          delimitedAtEnd = /\s$/.test(endRange);
-          if (!delimitedAtEnd) {
-            let container = endRange.endContainer;
-            let offset = endRange.endOffset;
-            if (container.nodeType == Node.TEXT_NODE &&
-                offset < container.textContent.length)
-              delimitedAtEnd = /\W/.test(container.textContent[offset]);
-            else
-              delimitedAtEnd = true;
-          }
-        }
-
-        if (delimitedAtStart && delimitedAtEnd) {
-          let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"]
-                           .getService(Ci.nsIURIFixup);
-          try {
-            uri = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE);
-          } catch (ex) {}
-        }
-      }
-
-      if (uri && uri.host) {
-        this.linkURI = uri;
-        this.linkURL = this.linkURI.spec;
-        this.linkText = linkText;
-        this.onPlainTextLink = true;
-      }
+      this.linkText = this.selectionInfo.linkText;
+      this.onPlainTextLink = true;
     }
 
     var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
     var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
     this.showItem("context-openlink", shouldShow && !isWindowPrivate);
     this.showItem("context-openlinkprivate", shouldShow);
     this.showItem("context-openlinkintab", shouldShow);
     this.showItem("context-openlinkincurrent", this.onPlainTextLink);
@@ -585,26 +533,29 @@ nsContextMenu.prototype = {
     this.inSrcdocFrame     = false;
     this.inSyntheticDoc    = false;
     this.hasBGImage        = false;
     this.bgImageURL        = "";
     this.onEditableArea    = false;
     this.isDesignMode      = false;
     this.onCTPPlugin       = false;
     this.canSpellCheck     = false;
-    this.textSelected      = getBrowserSelection();
+
+    if (this.isRemote) {
+      this.selectionInfo = gContextMenuContentData.selectionInfo;
+    } else {
+      this.selectionInfo = BrowserUtils.getSelectionDetails(window);
+    }
+
+    this.textSelected      = this.selectionInfo.text;
     this.isTextSelected    = this.textSelected.length != 0;
 
     // Remember the node that was clicked.
     this.target = aNode;
 
-    let [elt, win] = BrowserUtils.getFocusSync(document);
-    this.focusedWindow = win;
-    this.focusedElement = elt;
-
     let ownerDoc = this.target.ownerDocument;
     this.ownerDoc = ownerDoc;
 
     // If this is a remote context menu event, use the information from
     // gContextMenuContentData instead.
     if (this.isRemote) {
       this.browser = gContextMenuContentData.browser;
       this.principal = gContextMenuContentData.principal;
@@ -1540,21 +1491,16 @@ nsContextMenu.prototype = {
         if (!text || !text.match(/\S/))
           text = this.linkURL;
       }
     }
 
     return text;
   },
 
-  // Returns true if anything is selected.
-  isContentSelection: function() {
-    return !this.focusedWindow.getSelection().isCollapsed;
-  },
-
   isMediaURLReusable: function(aURL) {
     return !/^(?:blob|mediasource):/.test(aURL);
   },
 
   toString: function () {
     return "contextMenu.target     = " + this.target + "\n" +
            "contextMenu.onImage    = " + this.onImage + "\n" +
            "contextMenu.onLink     = " + this.onLink + "\n" +
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -3758,16 +3758,17 @@
                                           documentURIObject: documentURIObject,
                                           docLocation: aMessage.data.docLocation,
                                           charSet: aMessage.data.charSet,
                                           referrer: aMessage.data.referrer,
                                           referrerPolicy: aMessage.data.referrerPolicy,
                                           contentType: aMessage.data.contentType,
                                           contentDisposition: aMessage.data.contentDisposition,
                                           frameOuterWindowID: aMessage.data.frameOuterWindowID,
+                                          selectionInfo: aMessage.data.selectionInfo,
                                         };
               let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
               let event = gContextMenuContentData.event;
               popup.openPopupAtScreen(event.screenX, event.screenY, true);
               break;
             }
             case "DOMWebNotificationClicked": {
               let tab = this.getTabForBrowser(browser);
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -16,32 +16,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/PageThumbUtils.jsm");
 
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
                                      "@mozilla.org/xre/app-info;1",
                                      "nsICrashReporter");
 }
 
-let FocusSyncHandler = {
-  init: function() {
-    sendAsyncMessage("SetSyncHandler", {}, {handler: this});
-  },
-
-  getFocusedElementAndWindow: function() {
-    let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
-
-    let focusedWindow = {};
-    let elt = fm.getFocusedElementForWindow(content, true, focusedWindow);
-    return [elt, focusedWindow.value];
-  },
-};
-
-FocusSyncHandler.init();
-
 let WebProgressListener = {
   init: function() {
     this._filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
                      .createInstance(Ci.nsIWebProgress);
     this._filter.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_ALL);
 
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -1107,17 +1107,16 @@
               "_remoteWebProgress",
               "_remoteFinder",
               "_securityUI",
               "_documentURI",
               "_documentContentType",
               "_contentTitle",
               "_characterSet",
               "_contentPrincipal",
-              "_syncHandler",
               "_imageDocument",
               "_fullZoom",
               "_textZoom",
               "_isSyntheticDocument",
             ]);
           }
 
           var ourFieldValues = {};
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -158,22 +158,16 @@
       <property name="contentPrincipal"
                 onget="return this._contentPrincipal"
                 readonly="true"/>
 
       <property name="contentDocumentAsCPOW"
                 onget="return this.contentWindowAsCPOW ? this.contentWindowAsCPOW.document : null"
                 readonly="true"/>
 
-      <field name="_syncHandler">null</field>
-
-      <property name="syncHandler"
-                onget="return this._syncHandler"
-                readonly="true"/>
-
       <field name="_imageDocument">null</field>
 
       <property name="imageDocument"
                 onget="return this._imageDocument"
                 readonly="true"/>
 
       <field name="_fullZoom">1</field>
       <property name="fullZoom">
@@ -239,17 +233,16 @@
 
           let jsm = "resource://gre/modules/RemoteWebNavigation.jsm";
           let RemoteWebNavigation = Cu.import(jsm, {}).RemoteWebNavigation;
           this._remoteWebNavigation = new RemoteWebNavigation(this);
 
           this.messageManager.addMessageListener("Browser:Init", this);
           this.messageManager.addMessageListener("DOMTitleChanged", this);
           this.messageManager.addMessageListener("ImageDocumentLoaded", this);
-          this.messageManager.addMessageListener("SetSyncHandler", this);
           this.messageManager.addMessageListener("DocumentInserted", this);
           this.messageManager.addMessageListener("FullZoomChange", this);
           this.messageManager.addMessageListener("TextZoomChange", this);
           this.messageManager.addMessageListener("ZoomChangeUsingMouseWheel", this);
           this.messageManager.addMessageListener("DOMFullscreen:RequestExit", this);
           this.messageManager.addMessageListener("DOMFullscreen:RequestRollback", this);
           this.messageManager.loadFrameScript("chrome://global/content/browser-child.js", true);
 
@@ -305,20 +298,16 @@
               break;
             case "ImageDocumentLoaded":
               this._imageDocument = {
                 width: data.width,
                 height: data.height
               };
               break;
 
-            case "SetSyncHandler":
-              this._syncHandler = aMessage.objects.handler;
-              break;
-
             case "Forms:ShowDropDown": {
               Cu.import("resource://gre/modules/SelectParentHelper.jsm");
               let menulist = document.getElementById(this.getAttribute("selectmenulist"));
               SelectParentHelper.populate(menulist, data.options, data.selectedIndex);
               SelectParentHelper.open(this, menulist, data.rect);
               break;
             }
 
--- a/toolkit/modules/BrowserUtils.jsm
+++ b/toolkit/modules/BrowserUtils.jsm
@@ -89,41 +89,16 @@ this.BrowserUtils = {
     return Services.io.newFileURI(aFile);
   },
 
   makeURIFromCPOW: function(aCPOWURI) {
     return Services.io.newURI(aCPOWURI.spec, aCPOWURI.originCharset, null);
   },
 
   /**
-   * Return the current focus element and window. If the current focus
-   * is in a content process, then this function returns CPOWs
-   * (cross-process object wrappers) that refer to the focused
-   * items. Note that calling this function synchronously contacts the
-   * content process, which may block for a long time.
-   *
-   * @param document The document in question.
-   * @return [focusedElement, focusedWindow]
-   */
-  getFocusSync: function(document) {
-    let elt = document.commandDispatcher.focusedElement;
-    var window = document.commandDispatcher.focusedWindow;
-
-    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    if (elt instanceof window.XULElement &&
-        elt.localName == "browser" &&
-        elt.namespaceURI == XUL_NS &&
-        elt.getAttribute("remote")) {
-      [elt, window] = elt.syncHandler.getFocusedElementAndWindow();
-    }
-
-    return [elt, window];
-  },
-
-  /**
    * For a given DOM element, returns its position in "screen"
    * coordinates. In a content process, the coordinates returned will
    * be relative to the left/top of the tab. In the chrome process,
    * the coordinates are relative to the user's screen.
    */
   getElementBoundingScreenRect: function(aElement) {
     let rect = aElement.getBoundingClientRect();
     let window = aElement.ownerDocument.defaultView;
@@ -313,9 +288,111 @@ this.BrowserUtils = {
       }
       catch (e) {
         Cu.reportError(e);
         // If someone built with composer disabled, we can't get an editing session.
       }
     }
     return true;
   },
+
+  getSelectionDetails: function(topWindow, aCharLen) {
+    // selections of more than 150 characters aren't useful
+    const kMaxSelectionLen = 150;
+    const charLen = Math.min(aCharLen || kMaxSelectionLen, kMaxSelectionLen);
+
+    let focusedWindow = {};
+    let focusedElement = Services.focus.getFocusedElementForWindow(topWindow, true, focusedWindow);
+    focusedWindow = focusedWindow.value;
+
+    let selection = focusedWindow.getSelection();
+    let selectionStr = selection.toString();
+
+    let collapsed = selection.isCollapsed;
+
+    let url;
+    let linkText;
+    if (selectionStr) {
+      // Have some text, let's figure out if it looks like a URL that isn't
+      // actually a link.
+      linkText = selectionStr.trim();
+      if (/^(?:https?|ftp):/i.test(linkText)) {
+        try {
+          url = this.makeURI(linkText);
+        } catch (ex) {}
+      }
+      // Check if this could be a valid url, just missing the protocol.
+      else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) {
+        // Now let's see if this is an intentional link selection. Our guess is
+        // based on whether the selection begins/ends with whitespace or is
+        // preceded/followed by a non-word character.
+
+        // selection.toString() trims trailing whitespace, so we look for
+        // that explicitly in the first and last ranges.
+        let beginRange = selection.getRangeAt(0);
+        let delimitedAtStart = /^\s/.test(beginRange);
+        if (!delimitedAtStart) {
+          let container = beginRange.startContainer;
+          let offset = beginRange.startOffset;
+          if (container.nodeType == container.TEXT_NODE && offset > 0)
+            delimitedAtStart = /\W/.test(container.textContent[offset - 1]);
+          else
+            delimitedAtStart = true;
+        }
+
+        let delimitedAtEnd = false;
+        if (delimitedAtStart) {
+          let endRange = selection.getRangeAt(selection.rangeCount - 1);
+          delimitedAtEnd = /\s$/.test(endRange);
+          if (!delimitedAtEnd) {
+            let container = endRange.endContainer;
+            let offset = endRange.endOffset;
+            if (container.nodeType == container.TEXT_NODE &&
+                offset < container.textContent.length)
+              delimitedAtEnd = /\W/.test(container.textContent[offset]);
+            else
+              delimitedAtEnd = true;
+          }
+        }
+
+        if (delimitedAtStart && delimitedAtEnd) {
+          let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"]
+                           .getService(Ci.nsIURIFixup);
+          try {
+            url = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE);
+          } catch (ex) {}
+        }
+      }
+    }
+
+    // try getting a selected text in text input.
+    if (!selectionStr && focusedElement instanceof Ci.nsIDOMNSEditableElement) {
+      // Don't get the selection for password fields. See bug 565717.
+      if (focusedElement instanceof Ci.nsIDOMHTMLTextAreaElement ||
+          (focusedElement instanceof Ci.nsIDOMHTMLInputElement &&
+           focusedElement.mozIsTextField(true))) {
+        selectionStr = focusedElement.editor.selection.toString();
+      }
+    }
+
+    if (selectionStr) {
+      if (selectionStr.length > charLen) {
+        // only use the first charLen important chars. see bug 221361
+        var pattern = new RegExp("^(?:\\s*.){0," + charLen + "}");
+        pattern.test(selectionStr);
+        selectionStr = RegExp.lastMatch;
+      }
+
+      selectionStr = selectionStr.trim().replace(/\s+/g, " ");
+
+      if (selectionStr.length > charLen) {
+        selectionStr = selectionStr.substr(0, charLen);
+      }
+    }
+
+    if (url && !url.host) {
+      url = null;
+    }
+
+    return { text: selectionStr, docSelectionIsCollapsed: collapsed,
+             linkURL: url ? url.spec : null, linkText: url ? linkText : "" };
+  }
 };