gecko-dev/devtools/server/actors/layout.js
Patrick Brosset c1316a2855 Bug 1222409 - Listen to window resize events on server and use this to refresh style-inspector; r=bgrins
1 - Make the LayoutChangesObserver also send "resize" events; r=bgrins
The LayoutChangesObserver was originally made to observe all kinds of
layout-related events. So far, it was only observing reflows though.
This adds the capability to also observe resize events on the content
window.

2 - Removed the non-e10s rule/computed-views refreshing mechanism; r=bgrins
When the window is resized, the styles shown in the rule-view and
computed-view need to be updated (media-queries may be at play).
This was done before using a local-only, non-e10s solution. The
inspector-panel would listen to the resize event on the linkedBrowser
in the current tab.
This, obviously, did not work with e10s or across a remote connection.
This change just removes all of the code involved with this.
This won't cause any regression or backwards-compatibility problems as
a new server-driven resize observer is being put in place in this bug.
Even if you connected to an older server, you wouldn't see a difference
because the refresh-on-resize didn't work over remote connections already.

3 - Refresh the style-inspector when the LayoutChangesObserver detects resize
The implementation is simple, the inspector actor uses the
LayoutChangesObserver to detect window resize, and when it does, it
forwards the event to its front.
This is similar to how we deal with reflow events, except that for
reflows, the inspector actor (walker in this case), first filters on
the server to see if the reflow would indeed impact known nodes.
For resize events, it seemed more complex to do this kind of server
side filtering as this would involve remembering which node is currently
selected and which style were applied, and then compare that with the
new styles.

4 - Tests for the style-inspector refresh on window resize

--HG--
extra : commitid : 4AAhw4VBYII
extra : rebase_source : 412159e2a189541758613dd2fae954d973096f72
2015-11-26 12:18:17 +01:00

551 lines
16 KiB
JavaScript

/* 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";
/**
* About the types of objects in this file:
*
* - ReflowActor: the actor class used for protocol purposes.
* Mostly empty, just gets an instance of LayoutChangesObserver and forwards
* its "reflows" events to clients.
*
* - LayoutChangesObserver: extends Observable and uses the ReflowObserver, to
* track reflows on the page.
* Used by the LayoutActor, but is also exported on the module, so can be used
* by any other actor that needs it.
*
* - Observable: A utility parent class, meant at being extended by classes that
* need a to observe something on the tabActor's windows.
*
* - Dedicated observers: There's only one of them for now: ReflowObserver which
* listens to reflow events via the docshell,
* These dedicated classes are used by the LayoutChangesObserver.
*/
const {Ci, Cu} = require("chrome");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const protocol = require("devtools/server/protocol");
const {method, Arg} = protocol;
const events = require("sdk/event/core");
const Heritage = require("sdk/core/heritage");
const {setTimeout, clearTimeout} = require("sdk/timers");
const EventEmitter = require("devtools/shared/event-emitter");
/**
* The reflow actor tracks reflows and emits events about them.
*/
var ReflowActor = exports.ReflowActor = protocol.ActorClass({
typeName: "reflow",
events: {
/**
* The reflows event is emitted when reflows have been detected. The event
* is sent with an array of reflows that occured. Each item has the
* following properties:
* - start {Number}
* - end {Number}
* - isInterruptible {Boolean}
*/
"reflows": {
type: "reflows",
reflows: Arg(0, "array:json")
}
},
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._onReflow = this._onReflow.bind(this);
this.observer = getLayoutChangesObserver(tabActor);
this._isStarted = false;
},
/**
* The reflow actor is the first (and last) in its hierarchy to use
* protocol.js so it doesn't have a parent protocol actor that takes care of
* its lifetime. So it needs a disconnect method to cleanup.
*/
disconnect: function() {
this.destroy();
},
destroy: function() {
this.stop();
releaseLayoutChangesObserver(this.tabActor);
this.observer = null;
this.tabActor = null;
protocol.Actor.prototype.destroy.call(this);
},
/**
* Start tracking reflows and sending events to clients about them.
* This is a oneway method, do not expect a response and it won't return a
* promise.
*/
start: method(function() {
if (!this._isStarted) {
this.observer.on("reflows", this._onReflow);
this._isStarted = true;
}
}, {oneway: true}),
/**
* Stop tracking reflows and sending events to clients about them.
* This is a oneway method, do not expect a response and it won't return a
* promise.
*/
stop: method(function() {
if (this._isStarted) {
this.observer.off("reflows", this._onReflow);
this._isStarted = false;
}
}, {oneway: true}),
_onReflow: function(event, reflows) {
if (this._isStarted) {
events.emit(this, "reflows", reflows);
}
}
});
/**
* Usage example of the reflow front:
*
* let front = ReflowFront(toolbox.target.client, toolbox.target.form);
* front.on("reflows", this._onReflows);
* front.start();
* // now wait for events to come
*/
exports.ReflowFront = protocol.FrontClass(ReflowActor, {
initialize: function(client, {reflowActor}) {
protocol.Front.prototype.initialize.call(this, client, {actor: reflowActor});
this.manage(this);
},
destroy: function() {
protocol.Front.prototype.destroy.call(this);
},
});
/**
* Base class for all sorts of observers that need to listen to events on the
* tabActor's windows.
* @param {TabActor} tabActor
* @param {Function} callback Executed everytime the observer observes something
*/
function Observable(tabActor, callback) {
this.tabActor = tabActor;
this.callback = callback;
this._onWindowReady = this._onWindowReady.bind(this);
this._onWindowDestroyed = this._onWindowDestroyed.bind(this);
events.on(this.tabActor, "window-ready", this._onWindowReady);
events.on(this.tabActor, "window-destroyed", this._onWindowDestroyed);
}
Observable.prototype = {
/**
* Is the observer currently observing
*/
isObserving: false,
/**
* Stop observing and detroy this observer instance
*/
destroy: function() {
if (this.isDestroyed) {
return;
}
this.isDestroyed = true;
this.stop();
events.off(this.tabActor, "window-ready", this._onWindowReady);
events.off(this.tabActor, "window-destroyed", this._onWindowDestroyed);
this.callback = null;
this.tabActor = null;
},
/**
* Start observing whatever it is this observer is supposed to observe
*/
start: function() {
if (this.isObserving) {
return;
}
this.isObserving = true;
this._startListeners(this.tabActor.windows);
},
/**
* Stop observing
*/
stop: function() {
if (!this.isObserving) {
return;
}
this.isObserving = false;
if (this.tabActor.attached && this.tabActor.docShell) {
// It's only worth stopping if the tabActor is still attached
this._stopListeners(this.tabActor.windows);
}
},
_onWindowReady: function({window}) {
if (this.isObserving) {
this._startListeners([window]);
}
},
_onWindowDestroyed: function({window}) {
if (this.isObserving) {
this._stopListeners([window]);
}
},
_startListeners: function(windows) {
// To be implemented by sub-classes.
},
_stopListeners: function(windows) {
// To be implemented by sub-classes.
},
/**
* To be called by sub-classes when something has been observed
*/
notifyCallback: function(...args) {
this.isObserving && this.callback && this.callback.apply(null, args);
}
};
/**
* The LayouChangesObserver will observe reflows as soon as it is started.
* Some devtools actors may cause reflows and it may be wanted to "hide" these
* reflows from the LayouChangesObserver consumers.
* If this is the case, such actors should require this module and use this
* global function to turn the ignore mode on and off temporarily.
*
* Note that if a node is provided, it will be used to force a sync reflow to
* make sure all reflows which occurred before switching the mode on or off are
* either observed or ignored depending on the current mode.
*
* @param {Boolean} ignore
* @param {DOMNode} syncReflowNode The node to use to force a sync reflow
*/
var gIgnoreLayoutChanges = false;
exports.setIgnoreLayoutChanges = function(ignore, syncReflowNode) {
if (syncReflowNode) {
let forceSyncReflow = syncReflowNode.offsetWidth;
}
gIgnoreLayoutChanges = ignore;
};
/**
* The LayoutChangesObserver class is instantiated only once per given tab
* and is used to track reflows and dom and style changes in that tab.
* The LayoutActor uses this class to send reflow events to its clients.
*
* This class isn't exported on the module because it shouldn't be instantiated
* to avoid creating several instances per tabs.
* Use `getLayoutChangesObserver(tabActor)`
* and `releaseLayoutChangesObserver(tabActor)`
* which are exported to get and release instances.
*
* The observer loops every EVENT_BATCHING_DELAY ms and checks if layout changes
* have happened since the last loop iteration. If there are, it sends the
* corresponding events:
*
* - "reflows", with an array of all the reflows that occured,
* - "resizes", with an array of all the resizes that occured,
*
* @param {TabActor} tabActor
*/
function LayoutChangesObserver(tabActor) {
this.tabActor = tabActor;
this._startEventLoop = this._startEventLoop.bind(this);
this._onReflow = this._onReflow.bind(this);
this._onResize = this._onResize.bind(this);
// Creating the various observers we're going to need
// For now, just the reflow observer, but later we can add markupMutation,
// styleSheetChanges and styleRuleChanges
this.reflowObserver = new ReflowObserver(this.tabActor, this._onReflow);
this.resizeObserver = new WindowResizeObserver(this.tabActor, this._onResize);
EventEmitter.decorate(this);
}
exports.LayoutChangesObserver = LayoutChangesObserver;
LayoutChangesObserver.prototype = {
/**
* How long does this observer waits before emitting batched events.
* The lower the value, the more event packets will be sent to clients,
* potentially impacting performance.
* The higher the value, the more time we'll wait, this is better for
* performance but has an effect on how soon changes are shown in the toolbox.
*/
EVENT_BATCHING_DELAY: 300,
/**
* Destroying this instance of LayoutChangesObserver will stop the batched
* events from being sent.
*/
destroy: function() {
this.isObserving = false;
this.reflowObserver.destroy();
this.reflows = null;
this.resizeObserver.destroy();
this.hasResized = false;
this.tabActor = null;
},
start: function() {
if (this.isObserving) {
return;
}
this.isObserving = true;
this.reflows = [];
this.hasResized = false;
this._startEventLoop();
this.reflowObserver.start();
this.resizeObserver.start();
},
stop: function() {
if (!this.isObserving) {
return;
}
this.isObserving = false;
this._stopEventLoop();
this.reflows = [];
this.hasResized = false;
this.reflowObserver.stop();
this.resizeObserver.stop();
},
/**
* Start the event loop, which regularly checks if there are any observer
* events to be sent as batched events
* Calls itself in a loop.
*/
_startEventLoop: function() {
// Avoid emitting events if the tabActor has been detached (may happen
// during shutdown)
if (!this.tabActor || !this.tabActor.attached) {
return;
}
// Send any reflows we have
if (this.reflows && this.reflows.length) {
this.emit("reflows", this.reflows);
this.reflows = [];
}
// Send any resizes we have
if (this.hasResized) {
this.emit("resize");
this.hasResized = false;
}
this.eventLoopTimer = this._setTimeout(this._startEventLoop,
this.EVENT_BATCHING_DELAY);
},
_stopEventLoop: function() {
this._clearTimeout(this.eventLoopTimer);
},
// Exposing set/clearTimeout here to let tests override them if needed
_setTimeout: function(cb, ms) {
return setTimeout(cb, ms);
},
_clearTimeout: function(t) {
return clearTimeout(t);
},
/**
* Executed whenever a reflow is observed. Only stacks the reflow in the
* reflows array.
* The EVENT_BATCHING_DELAY loop will take care of it later.
* @param {Number} start When the reflow started
* @param {Number} end When the reflow ended
* @param {Boolean} isInterruptible
*/
_onReflow: function(start, end, isInterruptible) {
if (gIgnoreLayoutChanges) {
return;
}
// XXX: when/if bug 997092 gets fixed, we will be able to know which
// elements have been reflowed, which would be a nice thing to add here.
this.reflows.push({
start: start,
end: end,
isInterruptible: isInterruptible
});
},
/**
* Executed whenever a resize is observed. Only store a flag saying that a
* resize occured.
* The EVENT_BATCHING_DELAY loop will take care of it later.
*/
_onResize: function() {
if (gIgnoreLayoutChanges) {
return;
}
this.hasResized = true;
}
};
/**
* Get a LayoutChangesObserver instance for a given window. This function makes
* sure there is only one instance per window.
* @param {TabActor} tabActor
* @return {LayoutChangesObserver}
*/
var observedWindows = new Map();
function getLayoutChangesObserver(tabActor) {
let observerData = observedWindows.get(tabActor);
if (observerData) {
observerData.refCounting ++;
return observerData.observer;
}
let obs = new LayoutChangesObserver(tabActor);
observedWindows.set(tabActor, {
observer: obs,
// counting references allows to stop the observer when no tabActor owns an
// instance.
refCounting: 1
});
obs.start();
return obs;
}
exports.getLayoutChangesObserver = getLayoutChangesObserver;
/**
* Release a LayoutChangesObserver instance that was retrieved by
* getLayoutChangesObserver. This is required to ensure the tabActor reference
* is removed and the observer is eventually stopped and destroyed.
* @param {TabActor} tabActor
*/
function releaseLayoutChangesObserver(tabActor) {
let observerData = observedWindows.get(tabActor);
if (!observerData) {
return;
}
observerData.refCounting --;
if (!observerData.refCounting) {
observerData.observer.destroy();
observedWindows.delete(tabActor);
}
}
exports.releaseLayoutChangesObserver = releaseLayoutChangesObserver;
/**
* Reports any reflow that occurs in the tabActor's docshells.
* @extends Observable
* @param {TabActor} tabActor
* @param {Function} callback Executed everytime a reflow occurs
*/
function ReflowObserver(tabActor, callback) {
Observable.call(this, tabActor, callback);
}
ReflowObserver.prototype = Heritage.extend(Observable.prototype, {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
Ci.nsISupportsWeakReference]),
_startListeners: function(windows) {
for (let window of windows) {
let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
docshell.addWeakReflowObserver(this);
}
},
_stopListeners: function(windows) {
for (let window of windows) {
try {
let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
docshell.removeWeakReflowObserver(this);
} catch (e) {
// Corner cases where a global has already been freed may happen, in
// which case, no need to remove the observer.
}
}
},
reflow: function(start, end) {
this.notifyCallback(start, end, false);
},
reflowInterruptible: function(start, end) {
this.notifyCallback(start, end, true);
}
});
/**
* Reports window resize events on the tabActor's windows.
* @extends Observable
* @param {TabActor} tabActor
* @param {Function} callback Executed everytime a resize occurs
*/
function WindowResizeObserver(tabActor, callback) {
Observable.call(this, tabActor, callback);
this.onResize = this.onResize.bind(this);
}
WindowResizeObserver.prototype = Heritage.extend(Observable.prototype, {
_startListeners: function() {
this.listenerTarget.addEventListener("resize", this.onResize);
},
_stopListeners: function() {
this.listenerTarget.removeEventListener("resize", this.onResize);
},
onResize: function() {
this.notifyCallback();
},
get listenerTarget() {
// For the rootActor, return its window.
if (this.tabActor.isRootActor) {
return this.tabActor.window;
}
// Otherwise, get the tabActor's chromeEventHandler.
return this.tabActor.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
}
});