imported patch actor-autocomplete draft
authorNeil Deakin <neil@mozilla.com>
Fri, 16 Aug 2019 08:59:14 -0400
changeset 2220723 6243b3238a43d0bf912558f016a45371145a1600
parent 2220470 5d4cbfe103bbc517599231eb33d4f3ebbbcede40
child 2220724 e747e9c0d8a6ef9682964eb76476f205047720ba
push id406993
push userneil@mozilla.com
push dateFri, 16 Aug 2019 13:22:56 +0000
treeherdertry@e747e9c0d8a6 [default view] [failures only]
milestone70.0a1
imported patch actor-autocomplete
browser/components/BrowserGlue.jsm
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/api.js
browser/extensions/formautofill/content/FormAutofillFrameScript.js
browser/extensions/formautofill/content/customElements.js
toolkit/actors/AutoCompleteChild.jsm
toolkit/actors/AutoCompleteParent.jsm
toolkit/actors/moz.build
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/satchel/AutoCompletePopup.jsm
toolkit/components/satchel/moz.build
toolkit/components/satchel/test/browser/browser_close_tab.js
toolkit/components/satchel/test/test_datalist_shadow_dom.html
toolkit/content/browser-content.js
toolkit/modules/ActorManagerParent.jsm
toolkit/modules/AutoCompletePopupContent.jsm
toolkit/modules/moz.build
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -489,17 +489,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
     "resource:///modules/aboutpages/AboutNetErrorHandler.jsm",
   AboutPrivateBrowsingHandler:
     "resource:///modules/aboutpages/AboutPrivateBrowsingHandler.jsm",
   AboutProtectionsHandler:
     "resource:///modules/aboutpages/AboutProtectionsHandler.jsm",
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
-  AutoCompletePopup: "resource://gre/modules/AutoCompletePopup.jsm",
   Blocklist: "resource://gre/modules/Blocklist.jsm",
   BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.jsm",
   BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.jsm",
   BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   ContextualIdentityService:
     "resource://gre/modules/ContextualIdentityService.jsm",
   Corroborate: "resource://gre/modules/Corroborate.jsm",
@@ -1634,17 +1633,16 @@ BrowserGlue.prototype = {
         if (removalSuccessful && uninstalledValue == "True") {
           this._resetProfileNotification("uninstall");
         }
       }
     }
 
     this._checkForOldBuildUpdates();
 
-    AutoCompletePopup.init();
     // Check if Sync is configured
     if (Services.prefs.prefHasUserValue("services.sync.username")) {
       WeaveService.init();
     }
 
     PageThumbs.init();
 
     NewTabUtils.init();
@@ -1858,17 +1856,16 @@ BrowserGlue.prototype = {
       this.pingCentre.uninit();
     }
 
     PageThumbs.uninit();
     NewTabUtils.uninit();
     AboutNetErrorHandler.uninit();
     AboutPrivateBrowsingHandler.uninit();
     AboutProtectionsHandler.uninit();
-    AutoCompletePopup.uninit();
 
     Normandy.uninit();
     RFPHelper.uninit();
   },
 
   // Set up a listener to enable/disable the screenshots extension
   // based on its preference.
   _monitorScreenshotsPref() {
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -345,34 +345,27 @@ let ProfileAutocomplete = {
           break;
         }
         this._fillFromAutocompleteRow(FormAutofillContent.activeInput);
         break;
       }
     }
   },
 
-  _frameMMFromWindow(contentWindow) {
-    return contentWindow.docShell.messageManager;
+  getActorFromWindow(contentWindow) {
+    return contentWindow.getWindowGlobalChild().getActor("AutoComplete");
   },
 
   _getSelectedIndex(contentWindow) {
-    let mm = this._frameMMFromWindow(contentWindow);
-    let selectedIndexResult = mm.sendSyncMessage(
-      "FormAutoComplete:GetSelectedIndex",
-      {}
-    );
-    if (
-      selectedIndexResult.length != 1 ||
-      !Number.isInteger(selectedIndexResult[0])
-    ) {
+    let actor = this.getActorFromWindow(contentWindow);
+    if (!actor) {
       throw new Error("Invalid autocomplete selectedIndex");
     }
 
-    return selectedIndexResult[0];
+    return actor.selectedIndex;
   },
 
   _fillFromAutocompleteRow(focusedInput) {
     this.debug("_fillFromAutocompleteRow:", focusedInput);
     let formDetails = FormAutofillContent.activeFormDetails;
     if (!formDetails) {
       // The observer notification is for a different frame.
       return;
--- a/browser/extensions/formautofill/api.js
+++ b/browser/extensions/formautofill/api.js
@@ -18,16 +18,21 @@ ChromeUtils.defineModuleGetter(
   "FormAutofill",
   "resource://formautofill/FormAutofill.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "formAutofillParent",
   "resource://formautofill/FormAutofillParent.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "AutoCompleteParent",
+  "resource://gre/actors/AutoCompleteParent.jsm"
+);
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "resProto",
   "@mozilla.org/network/protocol;1?name=resource",
   "nsISubstitutingProtocolHandler"
 );
 
@@ -45,18 +50,17 @@ function insertStyleSheet(domWindow, url
 
   if (CACHED_STYLESHEETS.has(domWindow)) {
     CACHED_STYLESHEETS.get(domWindow).push(styleSheet);
   } else {
     CACHED_STYLESHEETS.set(domWindow, [styleSheet]);
   }
 }
 
-function onMaybeOpenPopup(evt) {
-  let domWindow = evt.target.ownerGlobal;
+function onMaybeOpenPopup(domWindow) {
   if (CACHED_STYLESHEETS.has(domWindow)) {
     // This window already has autofill stylesheets.
     return;
   }
 
   insertStyleSheet(domWindow, "chrome://formautofill/content/formautofill.css");
   insertStyleSheet(
     domWindow,
@@ -159,20 +163,17 @@ this.formautofill = class extends Extens
       );
     } else {
       Services.prefs.clearUserPref(
         "services.sync.engine.creditcards.available"
       );
     }
 
     // Listen for the autocomplete popup message to lazily append our stylesheet related to the popup.
-    Services.mm.addMessageListener(
-      "FormAutoComplete:MaybeOpenPopup",
-      onMaybeOpenPopup
-    );
+    AutoCompleteParent.addPopupStateListener(onMaybeOpenPopup);
 
     formAutofillParent.init().catch(Cu.reportError);
     Services.mm.loadFrameScript(
       "chrome://formautofill/content/FormAutofillFrameScript.js",
       true,
       true
     );
   }
@@ -188,20 +189,17 @@ this.formautofill = class extends Extens
     this.chromeHandle = null;
 
     if (this.autofillManifest) {
       Components.manager.removeBootstrappedManifestLocation(
         this.autofillManifest
       );
     }
 
-    Services.mm.removeMessageListener(
-      "FormAutoComplete:MaybeOpenPopup",
-      onMaybeOpenPopup
-    );
+    AutoCompleteParent.removePopupStateListener(onMaybeOpenPopup);
 
     for (let win of Services.wm.getEnumerator("navigator:browser")) {
       let cachedStyleSheets = CACHED_STYLESHEETS.get(win);
 
       if (!cachedStyleSheets) {
         continue;
       }
 
--- a/browser/extensions/formautofill/content/FormAutofillFrameScript.js
+++ b/browser/extensions/formautofill/content/FormAutofillFrameScript.js
@@ -26,26 +26,63 @@ ChromeUtils.defineModuleGetter(
   "FormAutofillContent",
   "resource://formautofill/FormAutofillContent.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "FormAutofillUtils",
   "resource://formautofill/FormAutofillUtils.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "AutoCompleteChild",
+  "resource://gre/actors/AutoCompleteChild.jsm"
+);
 
 /**
  * Handles content's interactions for the frame.
  */
 var FormAutofillFrameScript = {
   _nextHandleElement: null,
   _alreadyDOMContentLoaded: false,
   _hasDOMContentLoadedHandler: false,
   _hasPendingTask: false,
 
+  popupStateListener(messageName, data, target) {
+    if (!content || !FormAutofill.isAutofillEnabled) {
+      return;
+    }
+
+    const doc = target.document;
+    const { chromeEventHandler } = doc.ownerGlobal.docShell;
+
+    switch (messageName) {
+      case "FormAutoComplete:PopupClosed": {
+        FormAutofillContent.onPopupClosed(data.selectedRowStyle);
+        Services.tm.dispatchToMainThread(() => {
+          chromeEventHandler.removeEventListener(
+            "keydown",
+            FormAutofillContent._onKeyDown,
+            true
+          );
+        });
+
+        break;
+      }
+      case "FormAutoComplete:PopupOpened": {
+        chromeEventHandler.addEventListener(
+          "keydown",
+          FormAutofillContent._onKeyDown,
+          true
+        );
+        break;
+      }
+    }
+  },
+
   _doIdentifyAutofillFields() {
     if (this._hasPendingTask) {
       return;
     }
     this._hasPendingTask = true;
 
     setTimeout(() => {
       FormAutofillContent.identifyAutofillFields(this._nextHandleElement);
@@ -58,18 +95,19 @@ var FormAutofillFrameScript = {
     });
   },
 
   init() {
     addEventListener("focusin", this);
     addEventListener("DOMFormBeforeSubmit", this);
     addMessageListener("FormAutofill:PreviewProfile", this);
     addMessageListener("FormAutofill:ClearForm", this);
-    addMessageListener("FormAutoComplete:PopupClosed", this);
-    addMessageListener("FormAutoComplete:PopupOpened", this);
+
+    // XXXndeakin where does this get removed?
+    AutoCompleteChild.addPopupStateListener(this.popupStateListener);
   },
 
   handleEvent(evt) {
     if (!evt.isTrusted || !FormAutofill.isAutofillEnabled) {
       return;
     }
 
     switch (evt.type) {
@@ -130,44 +168,23 @@ var FormAutofillFrameScript = {
   },
 
   receiveMessage(message) {
     if (!FormAutofill.isAutofillEnabled) {
       return;
     }
 
     const doc = content.document;
-    const { chromeEventHandler } = doc.ownerGlobal.docShell;
 
     switch (message.name) {
       case "FormAutofill:PreviewProfile": {
         FormAutofillContent.previewProfile(doc);
         break;
       }
       case "FormAutofill:ClearForm": {
         FormAutofillContent.clearForm();
         break;
       }
-      case "FormAutoComplete:PopupClosed": {
-        FormAutofillContent.onPopupClosed(message.data.selectedRowStyle);
-        Services.tm.dispatchToMainThread(() => {
-          chromeEventHandler.removeEventListener(
-            "keydown",
-            FormAutofillContent._onKeyDown,
-            true
-          );
-        });
-
-        break;
-      }
-      case "FormAutoComplete:PopupOpened": {
-        chromeEventHandler.addEventListener(
-          "keydown",
-          FormAutofillContent._onKeyDown,
-          true
-        );
-        break;
-      }
     }
   },
 };
 
 FormAutofillFrameScript.init();
--- a/browser/extensions/formautofill/content/customElements.js
+++ b/browser/extensions/formautofill/content/customElements.js
@@ -9,16 +9,35 @@
 "use strict";
 
 // Wrap in a block to prevent leaking to window scope.
 (() => {
   const { Services } = ChromeUtils.import(
     "resource://gre/modules/Services.jsm"
   );
 
+  function sendMessageToBrowser(msgName, data) {
+    let { AutoCompleteParent } = ChromeUtils.import(
+      "resource://gre/actors/AutoCompleteParent.jsm"
+    );
+
+    let browser = AutoCompleteParent.getCurrentBrowser();
+    if (!browser) {
+      return;
+    }
+
+    if (browser.messageManager) {
+      browser.messageManager.sendAsyncMessage(msgName, data);
+    } else {
+      Cu.reportError(
+        `customElements.js: No messageManager for message "${msgName}"`
+      );
+    }
+  }
+
   class MozAutocompleteProfileListitemBase extends MozElements.MozRichlistitem {
     constructor() {
       super();
 
       /**
        * For form autofill, we want to unify the selection no matter by
        * keyboard navigation or mouseover in order not to confuse user which
        * profile preview is being shown. This field is set to true to indicate
@@ -106,20 +125,17 @@
 
     set selected(val) {
       if (val) {
         this.setAttribute("selected", "true");
       } else {
         this.removeAttribute("selected");
       }
 
-      let { AutoCompletePopup } = ChromeUtils.import(
-        "resource://gre/modules/AutoCompletePopup.jsm"
-      );
-      AutoCompletePopup.sendMessageToBrowser("FormAutofill:PreviewProfile");
+      sendMessageToBrowser("FormAutofill:PreviewProfile");
 
       return val;
     }
 
     get selected() {
       return this.getAttribute("selected") == "true";
     }
 
@@ -346,20 +362,17 @@
     constructor() {
       super();
 
       this.addEventListener("click", event => {
         if (event.button != 0) {
           return;
         }
 
-        let { AutoCompletePopup } = ChromeUtils.import(
-          "resource://gre/modules/AutoCompletePopup.jsm"
-        );
-        AutoCompletePopup.sendMessageToBrowser("FormAutofill:ClearForm");
+        sendMessageToBrowser("FormAutofill:ClearForm");
       });
     }
 
     connectedCallback() {
       if (this.delayConnectedCallback()) {
         return;
       }
 
rename from toolkit/modules/AutoCompletePopupContent.jsm
rename to toolkit/actors/AutoCompleteChild.jsm
--- a/toolkit/modules/AutoCompletePopupContent.jsm
+++ b/toolkit/actors/AutoCompleteChild.jsm
@@ -1,131 +1,239 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
 
-var EXPORTED_SYMBOLS = ["AutoCompletePopup"];
+var EXPORTED_SYMBOLS = ["AutoCompleteChild"];
 
 /* eslint no-unused-vars: ["error", {args: "none"}] */
 
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
 ChromeUtils.defineModuleGetter(
   this,
   "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm"
 );
 
-const MESSAGES = [
-  "FormAutoComplete:HandleEnter",
-  "FormAutoComplete:PopupClosed",
-  "FormAutoComplete:PopupOpened",
-  "FormAutoComplete:RequestFocus",
-];
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "formFill",
+  "@mozilla.org/satchel/form-fill-controller;1",
+  "nsIFormFillController"
+);
+
+let autoCompleteListeners = new Map();
+
+let currentActor = null;
 
 class AutoCompletePopup {
-  constructor(mm) {
-    this.mm = mm;
+  constructor(actor) {
+    this.actor = actor;
+  }
+
+  get input() {
+    return this.actor.input;
+  }
+
+  get overrideValue() {
+    return null;
+  }
+
+  set selectedIndex(index) {
+    return (this.actor.selectedIndex = index);
+  }
+
+  get selectedIndex() {
+    return this.actor.selectedIndex;
+  }
+
+  get popupOpen() {
+    return this.actor.popupOpen;
+  }
 
-    for (let messageName of MESSAGES) {
-      mm.addMessageListener(messageName, this);
-    }
+  openAutocompletePopup(input, element) {
+    return this.actor.openAutocompletePopup(input, element);
+  }
+
+  closePopup() {
+    this.actor.closePopup();
+  }
+
+  invalidate() {
+    this.actor.invalidate();
+  }
+
+  selectBy(reverse, page) {
+    this.actor.selectBy(reverse, page);
+  }
+}
+
+AutoCompletePopup.prototype.QueryInterface = ChromeUtils.generateQI([
+  Ci.nsIAutoCompletePopup,
+]);
+
+class AutoCompleteChild extends JSWindowActorChild {
+  constructor() {
+    super();
 
     this._input = null;
     this._popupOpen = false;
+
+    this.popup = new AutoCompletePopup(this);
+  }
+
+  detach() {
+    if (currentActor == this) {
+      formFill.detachFromBrowser(this.docShell);
+      currentActor = null;
+    }
+  }
+
+  static addPopupStateListener(listener) {
+    autoCompleteListeners.set(listener);
+  }
+
+  static removePopupChangeListener(listener) {
+    autoCompleteListeners.delete(listener);
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "DOMContentLoaded":
+      case "pageshow":
+        if (this.document.location == "about:blank") {
+          return;
+        }
+
+        // We need to wait for a content viewer to be available
+        // before we can attach our AutoCompletePopup handler,
+        // since nsFormFillController assumes one will exist
+        // when we call attachToBrowser.
+        if (currentActor != this) {
+          formFill.attachToBrowser(this.docShell, this.popup);
+          currentActor = this;
+        }
+        break;
+
+      case "pagehide":
+        this.detach();
+        break;
+    }
   }
 
   receiveMessage(message) {
     switch (message.name) {
       case "FormAutoComplete:HandleEnter": {
         this.selectedIndex = message.data.selectedIndex;
 
         let controller = Cc[
           "@mozilla.org/autocomplete/controller;1"
         ].getService(Ci.nsIAutoCompleteController);
         controller.handleEnter(message.data.isPopupSelection);
         break;
       }
 
       case "FormAutoComplete:PopupClosed": {
         this._popupOpen = false;
+        this.notifyListeners(message.name, message.data);
         break;
       }
 
       case "FormAutoComplete:PopupOpened": {
         this._popupOpen = true;
+        this.notifyListeners(message.name, message.data);
         break;
       }
 
-      case "FormAutoComplete:RequestFocus": {
+      case "FormAutoComplete:Focus": {
+        // XXXndeakin is this supposed to work? The parent and
+        // child messages didn't match before.
         if (this._input) {
-          this._input.focus();
+          // this._input.focus();
         }
         break;
       }
     }
   }
 
+  notifyListeners(messageName, data)
+  {
+    data.browsingContext = this.browsingContext;
+    for (let listener of autoCompleteListeners.entries()) {
+      listener[0](messageName, data, this.contentWindow);
+    }
+  }
+
   get input() {
     return this._input;
   }
-  get overrideValue() {
-    return null;
+
+  set selectedIndex(index) {
+    this.sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
   }
-  set selectedIndex(index) {
-    this.mm.sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
-  }
+
   get selectedIndex() {
     // selectedIndex getter must be synchronous because we need the
     // correct value when the controller is in controller::HandleEnter.
     // We can't easily just let the parent inform us the new value every
     // time it changes because not every action that can change the
     // selectedIndex is trivial to catch (e.g. moving the mouse over the
     // list).
-    return this.mm.sendSyncMessage("FormAutoComplete:GetSelectedIndex", {});
+    return Services.cpmm.sendSyncMessage("FormAutoComplete:GetSelectedIndex", {
+      browsingContext: this.browsingContext,
+    });
   }
+
   get popupOpen() {
     return this._popupOpen;
   }
 
   openAutocompletePopup(input, element) {
     if (this._popupOpen || !input) {
       return;
     }
 
     let rect = BrowserUtils.getElementBoundingScreenRect(element);
     let window = element.ownerGlobal;
     let dir = window.getComputedStyle(element).direction;
     let results = this.getResultsFromController(input);
 
-    this.mm.sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", {
+    this.sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", {
       results,
       rect,
       dir,
     });
+
     this._input = input;
   }
 
   closePopup() {
     // We set this here instead of just waiting for the
     // PopupClosed message to do it so that we don't end
     // up in a state where the content thinks that a popup
     // is open when it isn't (or soon won't be).
     this._popupOpen = false;
-    this.mm.sendAsyncMessage("FormAutoComplete:ClosePopup", {});
+    this.sendAsyncMessage("FormAutoComplete:ClosePopup", {});
   }
 
   invalidate() {
     if (this._popupOpen) {
       let results = this.getResultsFromController(this._input);
-      this.mm.sendAsyncMessage("FormAutoComplete:Invalidate", { results });
+      this.sendAsyncMessage("FormAutoComplete:Invalidate", { results });
     }
   }
 
   selectBy(reverse, page) {
-    this._index = this.mm.sendSyncMessage("FormAutoComplete:SelectBy", {
+    Services.cpmm.sendSyncMessage("FormAutoComplete:SelectBy", {
+      browsingContext: this.browsingContext,
       reverse,
       page,
     });
   }
 
   getResultsFromController(inputField) {
     let results = [];
 
rename from toolkit/components/satchel/AutoCompletePopup.jsm
rename to toolkit/actors/AutoCompleteParent.jsm
--- a/toolkit/components/satchel/AutoCompletePopup.jsm
+++ b/toolkit/actors/AutoCompleteParent.jsm
@@ -1,32 +1,78 @@
 /* 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/. */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["AutoCompletePopup"];
+var EXPORTED_SYMBOLS = ["AutoCompleteParent"];
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-// AutoCompleteResultView is an abstraction around a list of results
-// we got back up from browser-content.js. It implements enough of
-// nsIAutoCompleteController and nsIAutoCompleteInput to make the
-// richlistbox popup work.
+// Stores the browser and actor that has the active popup, used by formfill
+let currentBrowser = null;
+let currentActor = null;
+
+let autoCompleteListeners = new Set();
+
+// These are two synchronous messages sent by the child.
+// The browsingContext within the message data is either the one that has
+// the active autocomplete popup or the top-level of the one that has
+// the active autocomplete popup.
+Services.ppmm.addMessageListener(
+  "FormAutoComplete:GetSelectedIndex",
+  message => {
+    if (!currentActor ||
+        (currentActor.browsingContext != message.data.browsingContext &&
+         currentActor.browsingContext.top != message.data.browsingContext)) {
+      return;
+    }
+    
+    let actor = currentActor;
+    if (actor && actor.openedPopup) {
+      return actor.openedPopup.selectedIndex;
+    }
+
+    return -1;
+  }
+);
+
+Services.ppmm.addMessageListener("FormAutoComplete:SelectBy", message => {
+  if (!currentActor ||
+      (currentActor.browsingContext != message.data.browsingContext &&
+       currentActor.browsingContext.top != message.data.browsingContext)) {
+    return;
+  }
+
+  let actor = currentActor;
+  if (actor && actor.openedPopup) {
+    actor.openedPopup.selectBy(message.data.reverse, message.data.page);
+  }
+
+  return -1;
+});
+
+// AutoCompleteResultView is an abstraction around a list of results.
+// It implements enough of nsIAutoCompleteController and
+// nsIAutoCompleteInput to make the richlistbox popup work. Since only
+// one autocomplete popup should be open at a time, this is a singleton.
 var AutoCompleteResultView = {
   // nsISupports
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsIAutoCompleteController,
     Ci.nsIAutoCompleteInput,
   ]),
 
   // Private variables
   results: [],
 
+  // The AutoCompleteParent currently showing results or null otherwise.
+  currentActor: null,
+
   // nsIAutoCompleteController
   get matchCount() {
     return this.results.length;
   },
 
   getValueAt(index) {
     return this.results[index].value;
   },
@@ -52,144 +98,135 @@ var AutoCompleteResultView = {
     return this.results[index].style;
   },
 
   getImageAt(index) {
     return this.results[index].image;
   },
 
   handleEnter(aIsPopupSelection) {
-    AutoCompletePopup.handleEnter(aIsPopupSelection);
+    if (this.currentActor) {
+      this.currentActor.handleEnter(aIsPopupSelection);
+    }
   },
 
   stopSearch() {},
 
   searchString: "",
 
   // nsIAutoCompleteInput
   get controller() {
     return this;
   },
 
   get popup() {
     return null;
   },
 
   _focus() {
-    AutoCompletePopup.requestFocus();
+    if (this.currentActor) {
+      this.currentActor.requestFocus();
+    }
   },
 
   // Internal JS-only API
   clearResults() {
+    this.currentActor = null;
     this.results = [];
   },
 
-  setResults(results) {
+  setResults(actor, results) {
+    this.currentActor = actor;
     this.results = results;
   },
 };
 
-this.AutoCompletePopup = {
-  MESSAGES: [
-    "FormAutoComplete:SelectBy",
-    "FormAutoComplete:GetSelectedIndex",
-    "FormAutoComplete:SetSelectedIndex",
-    "FormAutoComplete:MaybeOpenPopup",
-    "FormAutoComplete:ClosePopup",
-    "FormAutoComplete:Disconnect",
-    "FormAutoComplete:RemoveEntry",
-    "FormAutoComplete:Invalidate",
-  ],
-
-  init() {
-    for (let msg of this.MESSAGES) {
-      Services.mm.addMessageListener(msg, this);
+class AutoCompleteParent extends JSWindowActorParent {
+  willDestroy() {
+    if (this.openedPopup) {
+      this.openedPopup.closePopup();
     }
-    Services.obs.addObserver(this, "message-manager-disconnect");
-  },
+  }
 
-  uninit() {
-    for (let msg of this.MESSAGES) {
-      Services.mm.removeMessageListener(msg, this);
-    }
-    Services.obs.removeObserver(this, "message-manager-disconnect");
-  },
+  static getCurrentBrowser() {
+    return currentBrowser ? currentBrowser.get() : null;
+  }
 
-  observe(subject, topic, data) {
-    switch (topic) {
-      case "message-manager-disconnect": {
-        if (this.openedPopup) {
-          this.openedPopup.closePopup();
-        }
-        break;
-      }
-    }
-  },
+  static addPopupStateListener(listener) {
+    autoCompleteListeners.add(listener);
+  }
+
+  static removePopupChangeListener(listener) {
+    autoCompleteListeners.delete(listener);
+  }
 
   handleEvent(evt) {
     switch (evt.type) {
       case "popupshowing": {
-        this.sendMessageToBrowser("FormAutoComplete:PopupOpened");
+        this.sendAsyncMessage("FormAutoComplete:PopupOpened", {});
         break;
       }
 
       case "popuphidden": {
         let selectedIndex = this.openedPopup.selectedIndex;
         let selectedRowStyle =
           selectedIndex != -1
             ? AutoCompleteResultView.getStyleAt(selectedIndex)
             : "";
-        this.sendMessageToBrowser("FormAutoComplete:PopupClosed", {
+        this.sendAsyncMessage("FormAutoComplete:PopupClosed", {
           selectedRowStyle,
         });
         AutoCompleteResultView.clearResults();
         // adjustHeight clears the height from the popup so that
         // we don't have a big shrink effect if we closed with a
         // large list, and then open on a small one.
         this.openedPopup.adjustHeight();
         this.openedPopup = null;
-        this.weakBrowser = null;
+        currentBrowser = null;
+        currentActor = null;
         evt.target.removeEventListener("popuphidden", this);
         evt.target.removeEventListener("popupshowing", this);
         break;
       }
     }
-  },
+  }
 
-  showPopupWithResults({ browser, rect, dir, results }) {
+  showPopupWithResults({ rect, dir, results }) {
     if (!results.length || this.openedPopup) {
       // We shouldn't ever be showing an empty popup, and if we
       // already have a popup open, the old one needs to close before
       // we consider opening a new one.
       return;
     }
 
+    let browser = this.browsingContext.top.embedderElement;
     let window = browser.ownerGlobal;
     // Also check window top in case this is a sidebar.
     if (
       Services.focus.activeWindow !== window.top &&
       Services.focus.focusedWindow.top !== window.top
     ) {
       // We were sent a message from a window or tab that went into the
       // background, so we'll ignore it for now.
       return;
     }
 
     // Non-empty result styles
     let resultStyles = new Set(results.map(r => r.style).filter(r => !!r));
-    this.weakBrowser = Cu.getWeakReference(browser);
+    currentBrowser = Cu.getWeakReference(browser);
+    currentActor = this;
     this.openedPopup = browser.autoCompletePopup;
     // the layout varies according to different result type
     this.openedPopup.setAttribute("resultstyles", [...resultStyles].join(" "));
     this.openedPopup.hidden = false;
     // don't allow the popup to become overly narrow
     this.openedPopup.setAttribute("width", Math.max(100, rect.width));
     this.openedPopup.style.direction = dir;
 
-    AutoCompleteResultView.setResults(results);
+    AutoCompleteResultView.setResults(this, results);
     this.openedPopup.view = AutoCompleteResultView;
     this.openedPopup.selectedIndex = -1;
 
     if (results.length) {
       // Reset fields that were set from the last time the search popup was open
       this.openedPopup.mInput = AutoCompleteResultView;
       // Temporarily increase the maxRows as we don't want to show
       // the scrollbar in login or form autofill popups.
@@ -211,84 +248,79 @@ this.AutoCompletePopup = {
         rect.height,
         false,
         false
       );
       this.openedPopup.invalidate();
     } else {
       this.closePopup();
     }
-  },
+  }
 
   invalidate(results) {
     if (!this.openedPopup) {
       return;
     }
 
     if (!results.length) {
       this.closePopup();
     } else {
-      AutoCompleteResultView.setResults(results);
+      AutoCompleteResultView.setResults(this, results);
       this.openedPopup.invalidate();
     }
-  },
+  }
 
   closePopup() {
     if (this.openedPopup) {
       // Note that hidePopup() closes the popup immediately,
       // so popuphiding or popuphidden events will be fired
       // and handled during this call.
       this.openedPopup.hidePopup();
     }
-  },
-
-  removeLogin(login) {
-    Services.logins.removeLogin(login);
-  },
+  }
 
   receiveMessage(message) {
-    if (!message.target.autoCompletePopup) {
+    let browser = this.browsingContext.top.embedderElement;
+    if (!browser || !browser.autoCompletePopup) {
+      // XXXndeakin this should be happening during a tabclose, but
+      // willDestroy doesn't seem to work for that.
+      if (this.openedPopup) {
+        this.openedPopup.closePopup();
+      }
+
       // Returning false to pacify ESLint, but this return value is
       // ignored by the messaging infrastructure.
       return false;
     }
 
     switch (message.name) {
       case "FormAutoComplete:SelectBy": {
         if (this.openedPopup) {
           this.openedPopup.selectBy(message.data.reverse, message.data.page);
         }
         break;
       }
 
-      case "FormAutoComplete:GetSelectedIndex": {
-        if (this.openedPopup) {
-          return this.openedPopup.selectedIndex;
-        }
-        // If the popup was closed, then the selection
-        // has not changed.
-        return -1;
-      }
-
       case "FormAutoComplete:SetSelectedIndex": {
         let { index } = message.data;
         if (this.openedPopup) {
           this.openedPopup.selectedIndex = index;
         }
         break;
       }
 
       case "FormAutoComplete:MaybeOpenPopup": {
         let { results, rect, dir } = message.data;
         this.showPopupWithResults({
-          browser: message.target,
           rect,
           dir,
           results,
         });
+
+        this.notifyListeners();
         break;
       }
 
       case "FormAutoComplete:Invalidate": {
         let { results } = message.data;
         this.invalidate(results);
         break;
       }
@@ -305,63 +337,47 @@ this.AutoCompletePopup = {
         // autocomplete and it would end up inheriting the existing data.
         AutoCompleteResultView.clearResults();
         break;
       }
     }
     // Returning false to pacify ESLint, but this return value is
     // ignored by the messaging infrastructure.
     return false;
-  },
+  }
+
+  notifyListeners()
+  {
+    let window = this.browsingContext.top.embedderElement.ownerGlobal;
+    for (let listener of autoCompleteListeners) {
+      listener(window);
+    }
+  }
 
   /**
    * Despite its name, this handleEnter is only called when the user clicks on
    * one of the items in the popup since the popup is rendered in the parent process.
    * The real controller's handleEnter is called directly in the content process
    * for other methods of completing a selection (e.g. using the tab or enter
    * keys) since the field with focus is in that process.
    * @param {boolean} aIsPopupSelection
    */
   handleEnter(aIsPopupSelection) {
     if (this.openedPopup) {
-      this.sendMessageToBrowser("FormAutoComplete:HandleEnter", {
+      this.sendAsyncMessage("FormAutoComplete:HandleEnter", {
         selectedIndex: this.openedPopup.selectedIndex,
         isPopupSelection: aIsPopupSelection,
       });
     }
-  },
+  }
+
+  stopSearch() {}
 
   /**
-   * If a browser exists that AutoCompletePopup knows about,
-   * sends it a message. Otherwise, this is a no-op.
-   *
-   * @param {string} msgName
-   *        The name of the message to send.
-   * @param {object} data
-   *        The optional data to send with the message.
-   */
-  sendMessageToBrowser(msgName, data) {
-    let browser = this.weakBrowser ? this.weakBrowser.get() : null;
-    if (!browser) {
-      return;
-    }
-
-    if (browser.messageManager) {
-      browser.messageManager.sendAsyncMessage(msgName, data);
-    } else {
-      Cu.reportError(
-        `AutoCompletePopup: No messageManager for message "${msgName}"`
-      );
-    }
-  },
-
-  stopSearch() {},
-
-  /**
-   * Sends a message to the browser requesting that the input
-   * that the AutoCompletePopup is open for be focused.
+   * Sends a message to the browser that is requesting the input
+   * that the open popup be focused.
    */
   requestFocus() {
     if (this.openedPopup) {
-      this.sendMessageToBrowser("FormAutoComplete:Focus");
+      this.sendAsyncMessage("FormAutoComplete:Focus");
     }
-  },
-};
+  }
+}
--- a/toolkit/actors/moz.build
+++ b/toolkit/actors/moz.build
@@ -15,16 +15,18 @@ with Files('KeyPressEventModelCheckerChi
 
 TESTING_JS_MODULES += [
     'TestChild.jsm',
     'TestParent.jsm',
 ]
 
 FINAL_TARGET_FILES.actors += [
     'AudioPlaybackChild.jsm',
+    'AutoCompleteChild.jsm',
+    'AutoCompleteParent.jsm',
     'AutoplayChild.jsm',
     'AutoplayParent.jsm',
     'BrowserElementChild.jsm',
     'BrowserElementParent.jsm',
     'ControllersChild.jsm',
     'DateTimePickerChild.jsm',
     'DateTimePickerParent.jsm',
     'ExtFindChild.jsm',
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -61,16 +61,21 @@ ChromeUtils.defineModuleGetter(
   "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "ContentDOMReference",
   "resource://gre/modules/ContentDOMReference.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "AutoCompleteChild",
+  "resource://gre/actors/AutoCompleteChild.jsm"
+);
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "gNetUtil",
   "@mozilla.org/network/util;1",
   "nsINetUtil"
 );
 
@@ -450,26 +455,31 @@ this.LoginManagerContent = {
         );
         if (inputElement) {
           this._generatedPasswordFilledOrEdited(inputElement);
         } else {
           log("Could not resolve inputElementIdentifier to a living element.");
         }
         break;
       }
+    }
+  },
 
+  popupStateListener(messageName, data, target)
+  {
+    switch (messageName) {
       case "FormAutoComplete:PopupOpened": {
-        let { chromeEventHandler } = msg.target.docShell;
+        let { chromeEventHandler } = target.docShell;
         chromeEventHandler.addEventListener("keydown", this._onKeyDown, true);
         break;
       }
 
       case "FormAutoComplete:PopupClosed": {
-        this._onPopupClosed(msg.data.selectedRowStyle, msg.target);
-        let { chromeEventHandler } = msg.target.docShell;
+        this._onPopupClosed(data.selectedRowStyle, target);
+        let { chromeEventHandler } = target.docShell;
         chromeEventHandler.removeEventListener(
           "keydown",
           this._onKeyDown,
           true
         );
         break;
       }
     }
@@ -535,18 +545,18 @@ this.LoginManagerContent = {
       actionOrigin,
       searchString: aSearchString,
       previousResult,
       isSecure: InsecurePasswordUtils.isFormSecure(form),
       isPasswordField: aElement.type == "password",
     };
 
     if (LoginHelper.showAutoCompleteFooter) {
-      messageManager.addMessageListener("FormAutoComplete:PopupOpened", this);
-      messageManager.addMessageListener("FormAutoComplete:PopupClosed", this);
+      // XXXndeakin where is this being removed?
+      AutoCompleteChild.addPopupStateListener(this.popupStateListener);
     }
 
     return this._sendRequest(
       messageManager,
       requestData,
       "PasswordManager:autoCompleteLogins",
       messageData
     );
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -14,21 +14,16 @@ const LoginInfo = new Components.Constru
   Ci.nsILoginInfo,
   "init"
 );
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
 
 ChromeUtils.defineModuleGetter(
   this,
-  "AutoCompletePopup",
-  "resource://gre/modules/AutoCompletePopup.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
   "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "LoginHelper",
   "resource://gre/modules/LoginHelper.jsm"
 );
@@ -181,17 +176,17 @@ this.LoginManagerParent = {
 
       case "PasswordManager:autoCompleteLogins": {
         this.doAutocompleteSearch(data, msg.target);
         break;
       }
 
       case "PasswordManager:removeLogin": {
         let login = LoginHelper.vanillaObjectToLogin(data.login);
-        AutoCompletePopup.removeLogin(login);
+        Services.logins.removeLogin(login);
         break;
       }
 
       case "PasswordManager:OpenPreferences": {
         LoginHelper.openPasswordManager(msg.target.ownerGlobal, {
           filterString: msg.data.hostname,
           entryPoint: msg.data.entryPoint,
         });
--- a/toolkit/components/satchel/moz.build
+++ b/toolkit/components/satchel/moz.build
@@ -23,17 +23,16 @@ SOURCES += [
     'nsFormFillController.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '../build',
 ]
 
 EXTRA_JS_MODULES += [
-    'AutoCompletePopup.jsm',
     'FormAutoComplete.jsm',
     'FormHistory.jsm',
     'FormHistoryStartup.jsm',
     'InputListAutoComplete.jsm',
     'nsFormAutoCompleteResult.jsm',
 ]
 
 XPCOM_MANIFESTS += [
--- a/toolkit/components/satchel/test/browser/browser_close_tab.js
+++ b/toolkit/components/satchel/test/browser/browser_close_tab.js
@@ -30,37 +30,17 @@ add_task(async function test() {
     });
 
     // show popup
     await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
     await TestUtils.waitForCondition(() => {
       return autoCompletePopup.popupOpen;
     });
 
-    let listener;
-    let errorLogPromise = new Promise(resolve => {
-      listener = ({ message }) => {
-        const ERROR_MSG =
-          "AutoCompletePopup: No messageManager for " +
-          'message "FormAutoComplete:PopupClosed"';
-        if (message.includes(ERROR_MSG)) {
-          Assert.ok(
-            true,
-            "Got the error message for inexistent messageManager."
-          );
-          Services.console.unregisterListener(listener);
-          resolve();
-        }
-      };
-    });
-    Services.console.registerListener(listener);
-
     gBrowser.removeCurrentTab();
 
     await TestUtils.waitForCondition(() => {
       return !autoCompletePopup.popupOpen;
     });
 
     Assert.ok(!autoCompletePopup.popupOpen, "Ensure the popup is closed.");
-
-    await errorLogPromise;
   });
 });
--- a/toolkit/components/satchel/test/test_datalist_shadow_dom.html
+++ b/toolkit/components/satchel/test/test_datalist_shadow_dom.html
@@ -83,16 +83,20 @@ function checkMenuEntries(expectedValues
   for (let i = 0; i < expectedValues.length; i++) {
     is(actualValues[i], expectedValues[i], testNum + " Checking menu entry #" + i);
   }
 }
 
 async function runTests() {
   testNum++;
   restoreForm();
+// XXXndeakin fix this
+SimpleTest.requestFlakyTimeout("Wait for UI test");
+await new Promise(r => { setTimeout(r, 1000); });
+
   synthesizeKey("KEY_ArrowDown");
   await expectPopup();
 
   checkMenuEntries(["First", "Second", "Secomundo"]);
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Enter");
   checkForm("First");
 
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -1,45 +1,29 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
 
 /* eslint-env mozilla/frame-script */
 /* eslint no-unused-vars: ["error", {args: "none"}] */
-/* global sendAsyncMessage */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-var { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
 var { ActorManagerChild } = ChromeUtils.import(
   "resource://gre/modules/ActorManagerChild.jsm"
 );
 
 ActorManagerChild.attach(this);
 
 ChromeUtils.defineModuleGetter(
   this,
-  "AutoCompletePopup",
-  "resource://gre/modules/AutoCompletePopupContent.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
   "AutoScrollController",
   "resource://gre/modules/AutoScrollController.jsm"
 );
 
-XPCOMUtils.defineLazyServiceGetter(
-  this,
-  "formFill",
-  "@mozilla.org/satchel/form-fill-controller;1",
-  "nsIFormFillController"
-);
-
 var global = this;
 
 var AutoScrollListener = {
   handleEvent(event) {
     if (event.isTrusted & !event.defaultPrevented && event.button == 1) {
       if (!this._controller) {
         this._controller = new AutoScrollController(global);
       }
@@ -48,53 +32,8 @@ var AutoScrollListener = {
   },
 };
 Services.els.addSystemEventListener(
   global,
   "mousedown",
   AutoScrollListener,
   true
 );
-
-let AutoComplete = {
-  _connected: false,
-
-  init() {
-    addEventListener("unload", this, { once: true });
-    addEventListener("DOMContentLoaded", this, { once: true });
-    // WebExtension browserAction is preloaded and does not receive DCL, wait
-    // on pageshow so we can hookup the formfill controller.
-    addEventListener("pageshow", this, { capture: true, once: true });
-
-    XPCOMUtils.defineLazyProxy(
-      this,
-      "popup",
-      () => new AutoCompletePopup(global),
-      { QueryInterface: null }
-    );
-    this.init = null;
-  },
-
-  handleEvent(event) {
-    switch (event.type) {
-      case "DOMContentLoaded":
-      case "pageshow":
-        // We need to wait for a content viewer to be available
-        // before we can attach our AutoCompletePopup handler,
-        // since nsFormFillController assumes one will exist
-        // when we call attachToBrowser.
-        if (!this._connected) {
-          formFill.attachToBrowser(docShell, this.popup);
-          this._connected = true;
-        }
-        break;
-
-      case "unload":
-        if (this._connected) {
-          formFill.detachFromBrowser(docShell);
-          this._connected = false;
-        }
-        break;
-    }
-  },
-};
-
-AutoComplete.init();
--- a/toolkit/modules/ActorManagerParent.jsm
+++ b/toolkit/modules/ActorManagerParent.jsm
@@ -98,16 +98,51 @@ var EXPORTED_SYMBOLS = ["ActorManagerPar
 const { ExtensionUtils } = ChromeUtils.import(
   "resource://gre/modules/ExtensionUtils.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const { DefaultMap } = ExtensionUtils;
 
 let ACTORS = {
+  AutoComplete: {
+    parent: {
+      moduleURI: "resource://gre/actors/AutoCompleteParent.jsm",
+      messages: [
+        "FormAutoComplete:SelectBy",
+        "FormAutoComplete:SetSelectedIndex",
+        "FormAutoComplete:MaybeOpenPopup",
+        "FormAutoComplete:Invalidate",
+        "FormAutoComplete:ClosePopup",
+        "FormAutoComplete:Disconnect",
+        // This message is also used, but is a synchronous call
+        // through the per-process message manager.
+        // "FormAutoComplete:GetSelectedIndex",
+      ],
+    },
+
+    child: {
+      moduleURI: "resource://gre/actors/AutoCompleteChild.jsm",
+      events: {
+        DOMContentLoaded: {},
+        pageshow: { capture: true },
+        pagehide: { capture: true },
+        unload: {},
+      },
+      messages: [
+        "FormAutoComplete:HandleEnter",
+        "FormAutoComplete:PopupClosed",
+        "FormAutoComplete:PopupOpened",
+        "FormAutoComplete:Focus",
+      ],
+    },
+
+    allFrames: true,
+  },
+
   Autoplay: {
     parent: {
       moduleURI: "resource://gre/actors/AutoplayParent.jsm",
     },
 
     child: {
       moduleURI: "resource://gre/actors/AutoplayChild.jsm",
       events: {
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -160,17 +160,16 @@ with Files('docs/**'):
 
 EXTRA_JS_MODULES += [
     'AboutPagesUtils.jsm',
     'ActorChild.jsm',
     'ActorManagerChild.jsm',
     'ActorManagerParent.jsm',
     'AppMenuNotifications.jsm',
     'AsyncPrefs.jsm',
-    'AutoCompletePopupContent.jsm',
     'AutoScrollController.jsm',
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CanonicalJSON.jsm',
     'CertUtils.jsm',
     'CharsetMenu.jsm',
     'Color.jsm',
     'Console.jsm',