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
--- 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 });