Bug 1296638 - AutoCompletePopup in browser-content.js and AutoCompletePopup.jsm need to have popupOpen be in sync. r?MattN draft
authorMike Conley <mconley@mozilla.com>
Sat, 17 Sep 2016 12:44:04 -0400
changeset 425590 65c262bfdda15d4050d008effa98413f21eb1ffc
parent 424914 7452437b3ab571b1d60aed4e973d82a1471f72b2
child 425591 0f696a06da6afc3d19739ab42ea2e477caa461d4
child 425611 0465a8a9d61c48c202e70d469b5cb4d43472b5b5
push id32459
push usermconley@mozilla.com
push dateSat, 15 Oct 2016 01:26:26 +0000
reviewersMattN
bugs1296638, 1294502
milestone52.0a1
Bug 1296638 - AutoCompletePopup in browser-content.js and AutoCompletePopup.jsm need to have popupOpen be in sync. r?MattN The opening of the AutoCompletePopup will always start from content, but closing the popup can occur in the parent (for example, if the user switches focus from the browser), or in content (the user hits Esc, for example, which tells the parent to close the popup). This relationship between content and the popup has been true for a while, but the patch in bug 1294502 didn't account for it. In particular, before this patch, it was possible for AutoCompletePopup in browser-content.js and AutoCompletePopup.jsm to get out of sync on whether or not the popup is open. Mainly, this is because the parent wasn't telling the content that the popup had hidden if the hide was initialized by the parent. The other reason, was because the popupOpen state in browser-content.js was being set immediately, instead of waiting for the parent to report that the popup had indeed opened or closed. MozReview-Commit-ID: CRkg49lP1Hd
toolkit/components/satchel/AutoCompletePopup.jsm
toolkit/content/browser-content.js
--- a/toolkit/components/satchel/AutoCompletePopup.jsm
+++ b/toolkit/components/satchel/AutoCompletePopup.jsm
@@ -94,20 +94,30 @@ this.AutoCompletePopup = {
 
   uninit: function() {
     for (let msg of this.MESSAGES) {
       Services.mm.removeMessageListener(msg, this);
     }
   },
 
   handleEvent: function(evt) {
-    if (evt.type === "popuphidden") {
-      this.openedPopup = null;
-      this.weakBrowser = null;
-      evt.target.removeEventListener("popuphidden", this);
+    switch (evt.type) {
+      case "popupshowing": {
+        this.sendMessageToBrowser("FormAutoComplete:PopupOpened");
+        break;
+      }
+
+      case "popuphidden": {
+        this.sendMessageToBrowser("FormAutoComplete:PopupClosed");
+        this.openedPopup = null;
+        this.weakBrowser = null;
+        evt.target.removeEventListener("popuphidden", this);
+        evt.target.removeEventListener("popupshowing", this);
+        break;
+      }
     }
   },
 
   // Along with being called internally by the receiveMessage handler,
   // this function is also called directly by the login manager, which
   // uses a single message to fill in the autocomplete results. See
   // "RemoteLogins:autoCompleteLogins".
   showPopupWithResults: function({ browser, rect, dir, results }) {
@@ -139,20 +149,21 @@ this.AutoCompletePopup = {
     this.openedPopup.selectedIndex = -1;
     this.openedPopup.invalidate();
 
     if (results.length) {
       // Reset fields that were set from the last time the search popup was open
       this.openedPopup.mInput = null;
       this.openedPopup.showCommentColumn = false;
       this.openedPopup.showImageColumn = false;
+      this.openedPopup.addEventListener("popuphidden", this);
+      this.openedPopup.addEventListener("popupshowing", this);
       this.openedPopup.openPopupAtScreenRect("after_start", rect.left, rect.top,
                                              rect.width, rect.height, false,
                                              false);
-      this.openedPopup.addEventListener("popuphidden", this);
     } else {
       this.closePopup();
     }
   },
 
   invalidate(results) {
     if (!this.openedPopup) {
       return;
@@ -166,17 +177,20 @@ this.AutoCompletePopup = {
       // tree to know the view has changed.
       this.openedPopup.view = AutoCompleteTreeView;
       this.openedPopup.invalidate();
     }
   },
 
   closePopup() {
     if (this.openedPopup) {
-      this.openedPopup.closePopup();
+      // Note that hidePopup() closes the popup immediately,
+      // so popuphiding or popuphidden events will be fired
+      // and handled during this call.
+      this.openedPopup.hidePopup();
     }
     AutoCompleteTreeView.clearResults();
   },
 
   removeLogin(login) {
     Services.logins.removeLogin(login);
   },
 
@@ -241,22 +255,36 @@ this.AutoCompletePopup = {
     // ignored by the messaging infrastructure.
     return false;
   },
 
   /**
    * Despite its name, handleEnter is what is called when the
    * user clicks on one of the items in the popup.
    */
-  handleEnter: function(aIsPopupSelection) {
+  handleEnter(aIsPopupSelection) {
+    if (this.openedPopup) {
+      this.sendMessageToBrowser("FormAutoComplete:HandleEnter", {
+        selectedIndex: this.openedPopup.selectedIndex,
+        isPopupSelection: aIsPopupSelection,
+      });
+    }
+  },
+
+  /**
+   * 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 && this.openedPopup) {
-      browser.messageManager.sendAsyncMessage(
-        "FormAutoComplete:HandleEnter",
-        { selectedIndex: this.openedPopup.selectedIndex,
-          isPopupSelection: aIsPopupSelection }
-      );
+    if (browser) {
+      browser.messageManager.sendAsyncMessage(msgName, data);
     }
   },
 
   stopSearch: function() {}
 }
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -1393,56 +1393,98 @@ addEventListener("MozApplicationManifest
   };
   sendAsyncMessage("MozApplicationManifest", info);
 }, false);
 
 let AutoCompletePopup = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]),
 
   _connected: false,
+
+  MESSAGES: [
+    "FormAutoComplete:HandleEnter",
+    "FormAutoComplete:PopupOpened",
+    "FormAutoComplete:PopupClosed",
+  ],
+
   init: function() {
-    // 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.
-    let onDCL = () => {
-      removeEventListener("DOMContentLoaded", onDCL);
-      // Hook up the form fill autocomplete controller.
-      let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"]
-                         .getService(Ci.nsIFormFillController);
-      controller.attachToBrowser(docShell,
-                                 this.QueryInterface(Ci.nsIAutoCompletePopup));
-      this._connected = true;
-    };
-    addEventListener("DOMContentLoaded", onDCL);
+    addEventListener("unload", this);
+    addEventListener("DOMContentLoaded", this);
+
+    for (let messageName of this.MESSAGES) {
+      addMessageListener(messageName, this);
+    }
 
     this._input = null;
     this._popupOpen = false;
-
-    addMessageListener("FormAutoComplete:HandleEnter", message => {
-      this.selectedIndex = message.data.selectedIndex;
-
-      let controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
-                  getService(Components.interfaces.nsIAutoCompleteController);
-      controller.handleEnter(message.data.isPopupSelection);
-    });
-
-    addEventListener("unload", function() {
-      AutoCompletePopup.destroy();
-    });
   },
 
   destroy: function() {
     if (this._connected) {
       let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"]
                          .getService(Ci.nsIFormFillController);
-
       controller.detachFromBrowser(docShell);
       this._connected = false;
     }
+
+    removeEventListener("unload", this);
+    removeEventListener("DOMContentLoaded", this);
+
+    for (let messageName of this.MESSAGES) {
+      removeMessageListener(messageName, this);
+    }
+  },
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "DOMContentLoaded": {
+        removeEventListener("DOMContentLoaded", this);
+
+        // 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.
+
+        // Hook up the form fill autocomplete controller.
+        let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"]
+                           .getService(Ci.nsIFormFillController);
+        controller.attachToBrowser(docShell,
+                                   this.QueryInterface(Ci.nsIAutoCompletePopup));
+        this._connected = true;
+        break;
+      }
+
+      case "unload": {
+        this.destroy();
+        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;
+        break;
+      }
+
+      case "FormAutoComplete:PopupOpened": {
+        this._popupOpen = true;
+        break;
+      }
+    }
   },
 
   get input () { return this._input; },
   get overrideValue () { return null; },
   set selectedIndex (index) {
     sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
   },
   get selectedIndex () {
@@ -1466,20 +1508,23 @@ let AutoCompletePopup = {
     let rect = BrowserUtils.getElementBoundingScreenRect(element);
     let window = element.ownerDocument.defaultView;
     let dir = window.getComputedStyle(element).direction;
     let results = this.getResultsFromController(input);
 
     sendAsyncMessage("FormAutoComplete:MaybeOpenPopup",
                      { results, rect, dir });
     this._input = input;
-    this._popupOpen = true;
   },
 
   closePopup: function () {
+    // 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;
     sendAsyncMessage("FormAutoComplete:ClosePopup", {});
   },
 
   invalidate: function () {
     if (this._popupOpen) {
       let results = this.getResultsFromController(this._input);
       sendAsyncMessage("FormAutoComplete:Invalidate", { results });