Commit ec8b2c4f authored by Neil Deakin's avatar Neil Deakin
Browse files

Bug 1573836, make autocomplete component fission compatible, r=mak,MattN

Differential Revision: https://phabricator.services.mozilla.com/D47093

--HG--
rename : toolkit/modules/AutoCompletePopupContent.jsm => toolkit/actors/AutoCompleteChild.jsm
rename : toolkit/components/satchel/AutoCompletePopup.jsm => toolkit/actors/AutoCompleteParent.jsm
extra : moz-landing-system : lando
parent 5b1e475e
......@@ -45,6 +45,7 @@ const whitelist = {
"resource:///modules/ContentMetaHandler.jsm",
"resource:///actors/LinkHandlerChild.jsm",
"resource:///actors/SearchTelemetryChild.jsm",
"resource://gre/actors/AutoCompleteChild.jsm",
"resource://gre/modules/ActorChild.jsm",
"resource://gre/modules/ActorManagerChild.jsm",
"resource://gre/modules/E10SUtils.jsm",
......
......@@ -470,7 +470,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
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",
......@@ -1618,7 +1617,6 @@ BrowserGlue.prototype = {
this._checkForOldBuildUpdates();
AutoCompletePopup.init();
// Check if Sync is configured
if (Services.prefs.prefHasUserValue("services.sync.username")) {
WeaveService.init();
......@@ -1879,7 +1877,6 @@ BrowserGlue.prototype = {
AboutNetErrorHandler.uninit();
AboutPrivateBrowsingHandler.uninit();
AboutProtectionsHandler.uninit();
AutoCompletePopup.uninit();
Normandy.uninit();
RFPHelper.uninit();
......
......@@ -39,7 +39,10 @@ add_task(async function setup() {
// AddonManager shuts down BrowserGlue will then try to uninit which will
// cause AutoComplete.jsm to throw an error.
// TODO: Fix in https://bugzilla.mozilla.org/show_bug.cgi?id=1543112.
PromiseTestUtils.whitelistRejectionsGlobally(/Component returned failure code/);
PromiseTestUtils.whitelistRejectionsGlobally(/A request was aborted/);
PromiseTestUtils.whitelistRejectionsGlobally(
/The operation failed for reasons unrelated/
);
const TOPICDATA_DISTRIBUTION_CUSTOMIZATION = "force-distribution-customization";
const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
......
......@@ -350,24 +350,17 @@ let ProfileAutocomplete = {
}
},
_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) {
......
......@@ -23,6 +23,11 @@ ChromeUtils.defineModuleGetter(
"formAutofillParent",
"resource://formautofill/FormAutofillParent.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"AutoCompleteParent",
"resource://gre/actors/AutoCompleteParent.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
......@@ -50,8 +55,7 @@ function insertStyleSheet(domWindow, url) {
}
}
function onMaybeOpenPopup(evt) {
let domWindow = evt.target.ownerGlobal;
function onMaybeOpenPopup(domWindow) {
if (CACHED_STYLESHEETS.has(domWindow)) {
// This window already has autofill stylesheets.
return;
......@@ -164,10 +168,7 @@ this.formautofill = class extends ExtensionAPI {
}
// 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(
......@@ -193,10 +194,7 @@ this.formautofill = class extends ExtensionAPI {
);
}
Services.mm.removeMessageListener(
"FormAutoComplete:MaybeOpenPopup",
onMaybeOpenPopup
);
AutoCompleteParent.removePopupStateListener(onMaybeOpenPopup);
for (let win of Services.wm.getEnumerator("navigator:browser")) {
let cachedStyleSheets = CACHED_STYLESHEETS.get(win);
......
......@@ -31,6 +31,11 @@ ChromeUtils.defineModuleGetter(
"FormAutofillUtils",
"resource://formautofill/FormAutofillUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"AutoCompleteChild",
"resource://gre/actors/AutoCompleteChild.jsm"
);
/**
* Handles content's interactions for the frame.
......@@ -41,6 +46,38 @@ var FormAutofillFrameScript = {
_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;
......@@ -61,26 +98,36 @@ var FormAutofillFrameScript = {
init() {
addEventListener("focusin", this);
addEventListener("DOMFormBeforeSubmit", this);
addEventListener("unload", this, { once: true });
addMessageListener("FormAutofill:PreviewProfile", this);
addMessageListener("FormAutofill:ClearForm", this);
addMessageListener("FormAutoComplete:PopupClosed", this);
addMessageListener("FormAutoComplete:PopupOpened", this);
AutoCompleteChild.addPopupStateListener(this.popupStateListener);
},
handleEvent(evt) {
if (!evt.isTrusted || !FormAutofill.isAutofillEnabled) {
if (!evt.isTrusted) {
return;
}
switch (evt.type) {
case "focusin": {
this.onFocusIn(evt);
if (FormAutofill.isAutofillEnabled) {
this.onFocusIn(evt);
}
break;
}
case "DOMFormBeforeSubmit": {
this.onDOMFormBeforeSubmit(evt);
if (FormAutofill.isAutofillEnabled) {
this.onDOMFormBeforeSubmit(evt);
}
break;
}
case "unload": {
AutoCompleteChild.removePopupStateListener(this.popupStateListener);
break;
}
default: {
throw new Error("Unexpected event type");
}
......@@ -135,7 +182,6 @@ var FormAutofillFrameScript = {
}
const doc = content.document;
const { chromeEventHandler } = doc.ownerGlobal.docShell;
switch (message.name) {
case "FormAutofill:PreviewProfile": {
......@@ -146,26 +192,6 @@ var FormAutofillFrameScript = {
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;
}
}
},
};
......
......@@ -14,6 +14,25 @@
"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();
......@@ -111,10 +130,7 @@
this.removeAttribute("selected");
}
let { AutoCompletePopup } = ChromeUtils.import(
"resource://gre/modules/AutoCompletePopup.jsm"
);
AutoCompletePopup.sendMessageToBrowser("FormAutofill:PreviewProfile");
sendMessageToBrowser("FormAutofill:PreviewProfile");
return val;
}
......@@ -350,10 +366,7 @@
return;
}
let { AutoCompletePopup } = ChromeUtils.import(
"resource://gre/modules/AutoCompletePopup.jsm"
);
AutoCompletePopup.sendMessageToBrowser("FormAutofill:ClearForm");
sendMessageToBrowser("FormAutofill:ClearForm");
});
}
......
......@@ -1251,13 +1251,13 @@ class SpecialPowersChild extends JSWindowActorChild {
);
}
attachFormFillControllerTo(window) {
this.getFormFillController().attachPopupElementToBrowser(
window.docShell,
this.getFormFillController().attachPopupElementToDocument(
window.document,
this._getAutoCompletePopup(window)
);
}
detachFormFillControllerFrom(window) {
this.getFormFillController().detachFromBrowser(window.docShell);
this.getFormFillController().detachFromDocument(window.document);
}
isBackButtonEnabled(window) {
return !this._getTopChromeWindow(window)
......
......@@ -3,33 +3,117 @@
* 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 Set();
class AutoCompletePopup {
constructor(mm) {
this.mm = mm;
constructor(actor) {
this.actor = actor;
}
for (let messageName of MESSAGES) {
mm.addMessageListener(messageName, this);
}
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;
}
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._attached = false;
}
static addPopupStateListener(listener) {
autoCompleteListeners.add(listener);
}
static removePopupStateListener(listener) {
autoCompleteListeners.delete(listener);
}
willDestroy() {
if (this._attached) {
formFill.detachFromDocument(this.document);
}
}
handleEvent(event) {
switch (event.type) {
case "DOMContentLoaded":
case "pageshow":
case "focus":
if (!this._attached && this.document.location != "about:blank") {
this._attached = true;
formFill.attachToDocument(this.document, new AutoCompletePopup(this));
}
if (event.type == "focus") {
formFill.handleFormEvent(event);
}
break;
case "pagehide":
case "unload":
if (this._attached) {
formFill.detachFromDocument(this.document);
this._attached = false;
}
// fall through
default:
formFill.handleFormEvent(event);
break;
}
}
receiveMessage(message) {
......@@ -46,32 +130,51 @@ class AutoCompletePopup {
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": {
// XXX See bug 1582722
// Before bug 1573836, the messages here didn't match
// ("FormAutoComplete:Focus" versus "FormAutoComplete:RequestFocus")
// so this was never called. However this._input is actually a
// nsIAutoCompleteInput, which doesn't have a focus() method, so it
// wouldn't have worked anyway. So for now, I have just disabled this.
/*
if (this._input) {
this._input.focus();
}
*/
break;
}
}
}
notifyListeners(messageName, data) {
for (let listener of autoCompleteListeners) {
try {
listener(messageName, data, this.contentWindow);
} catch (ex) {
Cu.reportError(ex);
}
}
}
get input() {
return this._input;
}
get overrideValue() {
return null;
}
set selectedIndex(index) {
this.mm.sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
this.sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
}
get selectedIndex() {
// selectedIndex getter must be synchronous because we need the
// correct value when the controller is in controller::HandleEnter.
......@@ -79,8 +182,11 @@ class AutoCompletePopup {
// 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;
}
......@@ -95,11 +201,12 @@ class AutoCompletePopup {
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;
}
......@@ -109,18 +216,19 @@ class AutoCompletePopup {
// 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,
});
......
......@@ -4,14 +4,59 @@
"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 currentBrowserWeakRef = null;
let currentActor = null;
let autoCompleteListeners = new Set();
function compareContext(message) {
if (
!currentActor ||
(currentActor.browsingContext != message.data.browsingContext &&
currentActor.browsingContext.top != message.data.browsingContext)
) {
return false;
}
return true;
}
// 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 (compareContext(message)) {
let actor = currentActor;
if (actor && actor.openedPopup) {
return actor.openedPopup.selectedIndex;
}
}
return -1;
}
);
Services.ppmm.addMessageListener("FormAutoComplete:SelectBy", message => {
if (compareContext(message)) {
let actor = currentActor;
if (actor && actor.openedPopup) {
actor.openedPopup.selectBy(message.data.reverse, message.data.page);
}
}
});
// 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([
......@@ -22,6 +67,9 @@ var AutoCompleteResultView = {
// Private variables
results: [],
// The AutoCompleteParent currently showing results or null otherwise.