Bug 1597765 - support auto scroll in out of process frames r=Gijs

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

--HG--
rename : toolkit/modules/AutoScrollController.jsm => toolkit/actors/AutoScrollChild.jsm
extra : moz-landing-system : lando
This commit is contained in:
Alexander Surkov 2019-12-19 02:35:57 +00:00
parent f5fb2ad06d
commit f9f8b6c906
8 changed files with 215 additions and 115 deletions

View File

@ -5,10 +5,12 @@
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var EXPORTED_SYMBOLS = ["AutoScrollController"];
var EXPORTED_SYMBOLS = ["AutoScrollChild"];
class AutoScrollChild extends JSWindowActorChild {
constructor() {
super();
class AutoScrollController {
constructor(global) {
this._scrollable = null;
this._scrolldir = "";
this._startX = null;
@ -18,10 +20,9 @@ class AutoScrollController {
this._lastFrame = null;
this._autoscrollHandledByApz = false;
this._scrollId = null;
this._global = global;
this.autoscrollLoop = this.autoscrollLoop.bind(this);
global.addMessageListener("Autoscroll:Stop", this);
this.observer = new AutoScrollObserver(this);
this.autoscrollLoop = this.autoscrollLoop.bind(this);
}
isAutoscrollBlocker(node) {
@ -145,16 +146,22 @@ class AutoScrollController {
this._scrolldir = direction;
this._scrollable = aNode.ownerGlobal;
} else if (aNode.ownerGlobal.frameElement) {
// FIXME(emilio): This won't work with Fission.
// Note, in case of out of process iframes frameElement is null, and
// a caller is supposed to communicate to iframe's parent on its own to
// support cross process scrolling.
this.findNearestScrollableElement(aNode.ownerGlobal.frameElement);
}
}
}
startScroll(event) {
async startScroll(event) {
this.findNearestScrollableElement(event.originalTarget);
if (!this._scrollable) {
this.sendAsyncMessage("Autoscroll:MaybeStartInParent", {
browsingContextId: this.browsingContext.id,
screenX: event.screenX,
screenY: event.screenY,
});
return;
}
@ -180,20 +187,23 @@ class AutoScrollController {
// No view ID - leave this._scrollId as null. Receiving side will check.
}
let presShellId = domUtils.getPresShellId();
let [result] = this._global.sendSyncMessage("Autoscroll:Start", {
scrolldir: this._scrolldir,
screenX: event.screenX,
screenY: event.screenY,
scrollId: this._scrollId,
presShellId,
});
if (!result.autoscrollEnabled) {
let { autoscrollEnabled, usingApz } = await this.sendQuery(
"Autoscroll:Start",
{
scrolldir: this._scrolldir,
screenX: event.screenX,
screenY: event.screenY,
scrollId: this._scrollId,
presShellId,
}
);
if (!autoscrollEnabled) {
this._scrollable = null;
return;
}
Services.els.addSystemEventListener(this._global, "mousemove", this, true);
this._global.addEventListener("pagehide", this, true);
Services.els.addSystemEventListener(this.document, "mousemove", this, true);
this.document.addEventListener("pagehide", this, true);
this._ignoreMouseEvents = true;
this._startX = event.screenX;
@ -202,9 +212,9 @@ class AutoScrollController {
this._screenY = event.screenY;
this._scrollErrorX = 0;
this._scrollErrorY = 0;
this._autoscrollHandledByApz = result.usingApz;
this._autoscrollHandledByApz = usingApz;
if (!result.usingApz) {
if (!usingApz) {
// If the browser didn't hand the autoscroll off to APZ,
// scroll here in the main thread.
this.startMainThreadScroll();
@ -212,12 +222,16 @@ class AutoScrollController {
// Even if the browser did hand the autoscroll to APZ,
// APZ might reject it in which case it will notify us
// and we need to take over.
Services.obs.addObserver(this, "autoscroll-rejected-by-apz");
Services.obs.addObserver(this.observer, "autoscroll-rejected-by-apz");
}
if (Cu.isInAutomation) {
Services.obs.notifyObservers(content, "autoscroll-start");
}
}
startMainThreadScroll() {
let content = this._global.content;
let content = this.document.defaultView;
this._lastFrame = content.performance.now();
content.requestAnimationFrame(this.autoscrollLoop);
}
@ -228,14 +242,17 @@ class AutoScrollController {
this._scrollable = null;
Services.els.removeSystemEventListener(
this._global,
this.document,
"mousemove",
this,
true
);
this._global.removeEventListener("pagehide", this, true);
this.document.removeEventListener("pagehide", this, true);
if (this._autoscrollHandledByApz) {
Services.obs.removeObserver(this, "autoscroll-rejected-by-apz");
Services.obs.removeObserver(
this.observer,
"autoscroll-rejected-by-apz"
);
}
}
}
@ -306,6 +323,8 @@ class AutoScrollController {
this._screenY = event.screenY;
} else if (event.type == "mousedown") {
if (
event.isTrusted & !event.defaultPrevented &&
event.button == 1 &&
!this._scrollable &&
!this.isAutoscrollBlocker(event.originalTarget)
) {
@ -315,14 +334,28 @@ class AutoScrollController {
if (this._scrollable) {
var doc = this._scrollable.ownerDocument || this._scrollable.document;
if (doc == event.target) {
this._global.sendAsyncMessage("Autoscroll:Cancel");
this.sendAsyncMessage("Autoscroll:Cancel");
this.stopScroll();
}
}
}
}
receiveMessage(msg) {
let data = msg.data;
switch (msg.name) {
case "Autoscroll:MaybeStart":
for (let child of this.browsingContext.getChildren()) {
if (data.browsingContextId == child.id) {
this.startScroll({
screenX: data.screenX,
screenY: data.screenY,
originalTarget: child.embedderElement,
});
break;
}
}
break;
case "Autoscroll:Stop": {
this.stopScroll();
break;
@ -330,14 +363,24 @@ class AutoScrollController {
}
}
observe(subject, topic, data) {
if (topic === "autoscroll-rejected-by-apz") {
// The caller passes in the scroll id via 'data'.
if (data == this._scrollId) {
this._autoscrollHandledByApz = false;
this.startMainThreadScroll();
Services.obs.removeObserver(this, "autoscroll-rejected-by-apz");
}
rejectedByApz(data) {
// The caller passes in the scroll id via 'data'.
if (data == this._scrollId) {
this._autoscrollHandledByApz = false;
this.startMainThreadScroll();
Services.obs.removeObserver(this.observer, "autoscroll-rejected-by-apz");
}
}
}
class AutoScrollObserver {
constructor(actor) {
this.actor = actor;
}
observe(subject, topic, data) {
if (topic === "autoscroll-rejected-by-apz") {
this.actor.rejectedByApz(data);
}
}
}

View File

@ -0,0 +1,33 @@
/* vim: set ts=2 sw=2 sts=2 et tw=80: */
/* 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 = ["AutoScrollParent"];
class AutoScrollParent extends JSWindowActorParent {
receiveMessage(msg) {
let browser = this.manager.browsingContext.top.embedderElement;
if (!browser) {
return null;
}
let data = msg.data;
switch (msg.name) {
case "Autoscroll:Start":
return Promise.resolve(browser.startScroll(data));
case "Autoscroll:MaybeStartInParent":
let parent = this.browsingContext.parent;
if (parent) {
let actor = parent.currentWindowGlobal.getActor("AutoScroll");
actor.sendAsyncMessage("Autoscroll:MaybeStart", data);
}
break;
case "Autoscroll:Cancel":
browser.cancelScroll();
break;
}
return null;
}
}

View File

@ -7,6 +7,9 @@
with Files('**'):
BUG_COMPONENT = ('Toolkit', 'General')
with Files('AutoScroll*.jsm'):
BUG_COMPONENT = ('Core', 'Panning and Zooming')
with Files('Finder*.jsm'):
BUG_COMPONENT = ('Toolkit', 'Find Toolbar')
@ -25,6 +28,8 @@ FINAL_TARGET_FILES.actors += [
'AutoCompleteParent.jsm',
'AutoplayChild.jsm',
'AutoplayParent.jsm',
'AutoScrollChild.jsm',
'AutoScrollParent.jsm',
'BrowserElementChild.jsm',
'BrowserElementParent.jsm',
'ControllersChild.jsm',

View File

@ -6,34 +6,11 @@
/* eslint-env mozilla/frame-script */
/* eslint no-unused-vars: ["error", {args: "none"}] */
/* exported Services */
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { ActorManagerChild } = ChromeUtils.import(
"resource://gre/modules/ActorManagerChild.jsm"
);
ActorManagerChild.attach(this);
ChromeUtils.defineModuleGetter(
this,
"AutoScrollController",
"resource://gre/modules/AutoScrollController.jsm"
);
var global = this;
var AutoScrollListener = {
handleEvent(event) {
if (event.isTrusted & !event.defaultPrevented && event.button == 1) {
if (!this._controller) {
this._controller = new AutoScrollController(global);
}
this._controller.handleEvent(event);
}
},
};
Services.els.addSystemEventListener(
global,
"mousedown",
AutoScrollListener,
true
);

View File

@ -160,6 +160,26 @@ body > div > div {width: 1000px;height: 1000px;}\
expected: expectScrollNone,
testwindow: true,
},
{
// Test: scroll is initiated in out of process iframe having no scrollable area
dataUri:
"data:text/html," +
encodeURIComponent(`
<!doctype html>
<head><meta content="text/html;charset=utf-8"></head><body>
<div id="scroller" style="width: 300px; height: 300px; overflow-y: scroll; overflow-x: hidden; border: solid 1px blue;">
<iframe id="noscroll-outofprocess-iframe" src="https://example.com/browser/toolkit/content/tests/browser/file_contentTitle.html"
style="border: solid 1px green; margin: 2px;"></iframe>
<div style="width: 100%; height: 200px;"></div>
</div></body>
`),
},
{
elem: "noscroll-outofprocess-iframe",
// We expect the div to scroll vertically, not the iframe's window.
expected: expectScrollVert,
scrollable: "scroller",
},
];
for (let test of allTests) {
@ -187,19 +207,28 @@ body > div > div {width: 1000px;height: 1000px;}\
// This ensures bug 605127 is fixed: pagehide in an unrelated document
// should not cancel the autoscroll.
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
var iframe = content.document.getElementById("iframe");
await ContentTask.spawn(
gBrowser.selectedBrowser,
{ waitForAutoScrollStart: test.expected != expectScrollNone },
async ({ waitForAutoScrollStart }) => {
var iframe = content.document.getElementById("iframe");
if (iframe) {
var e = new iframe.contentWindow.PageTransitionEvent("pagehide", {
bubbles: true,
cancelable: true,
persisted: false,
});
iframe.contentDocument.dispatchEvent(e);
iframe.contentDocument.documentElement.dispatchEvent(e);
if (iframe) {
var e = new iframe.contentWindow.PageTransitionEvent("pagehide", {
bubbles: true,
cancelable: true,
persisted: false,
});
iframe.contentDocument.dispatchEvent(e);
iframe.contentDocument.documentElement.dispatchEvent(e);
}
if (waitForAutoScrollStart) {
await new Promise(resolve =>
Services.obs.addObserver(resolve, "autoscroll-start")
);
}
}
});
);
is(
document.activeElement,
@ -265,7 +294,7 @@ body > div > div {width: 1000px;height: 1000px;}\
{
scrollVert,
scrollHori,
elemid: test.elem,
elemid: test.scrollable || test.elem,
checkWindow: test.testwindow,
},
],

View File

@ -1344,8 +1344,6 @@
"PopupBlocking:UpdateBlockedPopups",
this
);
this.messageManager.addMessageListener("Autoscroll:Start", this);
this.messageManager.addMessageListener("Autoscroll:Cancel", this);
this.messageManager.addMessageListener(
"UnselectedTabHover:Toggle",
this
@ -1418,41 +1416,6 @@
this.updateBlockedPopups();
break;
}
case "Autoscroll:Start": {
if (!this.autoscrollEnabled) {
return { autoscrollEnabled: false, usingApz: false };
}
this.startScroll(data.scrolldir, data.screenX, data.screenY);
let usingApz = false;
if (
this.isRemoteBrowser &&
data.scrollId != null &&
this.mPrefs.getBoolPref("apz.autoscroll.enabled", false)
) {
let { remoteTab } = this.frameLoader;
if (remoteTab) {
// If APZ is handling the autoscroll, it may decide to cancel
// it of its own accord, so register an observer to allow it
// to notify us of that.
var os = Services.obs;
os.addObserver(this.observer, "apz:cancel-autoscroll", true);
usingApz = remoteTab.startApzAutoscroll(
data.screenX,
data.screenY,
data.scrollId,
data.presShellId
);
}
// Save the IDs for later
this._autoScrollScrollId = data.scrollId;
this._autoScrollPresShellId = data.presShellId;
}
return { autoscrollEnabled: true, usingApz };
}
case "Autoscroll:Cancel":
this._autoScrollPopup.hidePopup();
break;
case "UnselectedTabHover:Toggle":
this._shouldSendUnselectedTabHover = data.enable
? ++this._unselectedTabHoverMessageListenerCount > 0
@ -1659,7 +1622,8 @@
window.removeEventListener("keydown", this, true);
window.removeEventListener("keypress", this, true);
window.removeEventListener("keyup", this, true);
this.messageManager.sendAsyncMessage("Autoscroll:Stop");
this.sendMessageToActor("Autoscroll:Stop", {}, "AutoScroll", true);
try {
Services.obs.removeObserver(this.observer, "apz:cancel-autoscroll");
@ -1690,7 +1654,11 @@
return popup;
}
startScroll(scrolldir, screenX, screenY) {
startScroll({ scrolldir, screenX, screenY, scrollId, presShellId }) {
if (!this.autoscrollEnabled) {
return { autoscrollEnabled: false, usingApz: false };
}
const POPUP_SIZE = 32;
if (!this._autoScrollPopup) {
if (this.hasAttribute("autoscrollpopup")) {
@ -1772,6 +1740,40 @@
window.addEventListener("keydown", this, true);
window.addEventListener("keypress", this, true);
window.addEventListener("keyup", this, true);
let usingApz = false;
if (
this.isRemoteBrowser &&
scrollId != null &&
this.mPrefs.getBoolPref("apz.autoscroll.enabled", false)
) {
let { remoteTab } = this.frameLoader;
if (remoteTab) {
// If APZ is handling the autoscroll, it may decide to cancel
// it of its own accord, so register an observer to allow it
// to notify us of that.
Services.obs.addObserver(
this.observer,
"apz:cancel-autoscroll",
true
);
usingApz = remoteTab.startApzAutoscroll(
screenX,
screenY,
scrollId,
presShellId
);
}
// Save the IDs for later
this._autoScrollScrollId = scrollId;
this._autoScrollPresShellId = presShellId;
}
return { autoscrollEnabled: true, usingApz };
}
cancelScroll() {
this._autoScrollPopup.hidePopup();
}
handleEvent(aEvent) {

View File

@ -90,6 +90,21 @@ let ACTORS = {
allFrames: true,
},
AutoScroll: {
parent: {
moduleURI: "resource://gre/actors/AutoScrollParent.jsm",
},
child: {
moduleURI: "resource://gre/actors/AutoScrollChild.jsm",
events: {
mousedown: { capture: true, mozSystemGroup: true },
},
},
allFrames: true,
},
BrowserElement: {
parent: {
moduleURI: "resource://gre/actors/BrowserElementParent.jsm",

View File

@ -48,9 +48,6 @@ with Files('tests/xpcshell/test_UpdateUtils*.js'):
with Files('AsyncPrefs.jsm'):
BUG_COMPONENT = ('Core', 'Security: Process Sandboxing')
with Files('AutoScrollController.jsm'):
BUG_COMPONENT = ('Core', 'Panning and Zooming')
with Files('CharsetMenu.jsm'):
BUG_COMPONENT = ('Firefox', 'Toolbars and Customization')
@ -165,7 +162,6 @@ EXTRA_JS_MODULES += [
'ActorManagerParent.jsm',
'AppMenuNotifications.jsm',
'AsyncPrefs.jsm',
'AutoScrollController.jsm',
'BinarySearch.jsm',
'BrowserUtils.jsm',
'CanonicalJSON.jsm',