mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 997198 - Create a standalone reflow actor; r=bgrins
This commit is contained in:
parent
8bdbedad0f
commit
a1a0bf1ec7
@ -83,9 +83,7 @@ function*() {
|
||||
|
||||
is(editor.value, "20px", "Should have the right value in the editor.");
|
||||
is(getStyle(node, "margin-left"), "20px", "Should have updated the margin.");
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, view);
|
||||
yield waitForUpdate();
|
||||
|
||||
is(getStyle(node, "margin-left"), "20px", "Should be the right margin-top on the element.")
|
||||
is(span.textContent, 20, "Should have the right value in the box model.");
|
||||
@ -139,7 +137,6 @@ function*() {
|
||||
is(getStyle(node, "margin-right"), "", "Should have updated the margin.");
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, view);
|
||||
yield waitForUpdate();
|
||||
|
||||
is(getStyle(node, "margin-right"), "", "Should be the right margin-top on the element.")
|
||||
is(span.textContent, 10, "Should have the right value in the box model.");
|
||||
|
@ -48,7 +48,6 @@ function*() {
|
||||
is(getStyle(node, "padding-bottom"), "7px", "Should have updated the padding");
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, view);
|
||||
yield waitForUpdate();
|
||||
|
||||
is(getStyle(node, "padding-bottom"), "7px", "Should be the right padding.")
|
||||
is(span.textContent, 7, "Should have the right value in the box model.");
|
||||
@ -102,7 +101,6 @@ function*() {
|
||||
is(getStyle(node, "padding-left"), "", "Should have updated the padding");
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, view);
|
||||
yield waitForUpdate();
|
||||
|
||||
is(getStyle(node, "padding-left"), "", "Should be the right padding.")
|
||||
is(span.textContent, 3, "Should have the right value in the box model.");
|
||||
|
@ -54,7 +54,6 @@ function*() {
|
||||
is(getStyle(node, "padding-top"), "1em", "Should have updated the padding.");
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, view);
|
||||
yield waitForUpdate();
|
||||
|
||||
is(getStyle(node, "padding-top"), "1em", "Should be the right padding.")
|
||||
is(span.textContent, 16, "Should have the right value in the box model.");
|
||||
@ -81,7 +80,6 @@ function*() {
|
||||
is(getStyle(node, "border-bottom-width"), "0px", "Should have updated the border.");
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, view);
|
||||
yield waitForUpdate();
|
||||
|
||||
is(getStyle(node, "border-bottom-width"), "0px", "Should be the right border-bottom-width.")
|
||||
is(span.textContent, 0, "Should have the right value in the box model.");
|
||||
@ -102,7 +100,6 @@ function*() {
|
||||
is(editor.value, "2em", "Should have the right value in the editor.");
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {}, view);
|
||||
yield waitForUpdate();
|
||||
|
||||
is(getStyle(node, "padding-right"), "", "Should be the right padding.")
|
||||
is(span.textContent, 32, "Should have the right value in the box model.");
|
||||
|
@ -18,6 +18,7 @@ Cu.import("resource://gre/modules/devtools/Console.jsm");
|
||||
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
|
||||
const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
|
||||
const {ReflowFront} = devtools.require("devtools/server/actors/layout");
|
||||
|
||||
const NUMERIC = /^-?[\d\.]+$/;
|
||||
const LONG_TEXT_ROTATE_LIMIT = 3;
|
||||
@ -90,14 +91,17 @@ EditingSession.prototype = {
|
||||
let modifications = this._rules[0].startModifyingProperties();
|
||||
|
||||
for (let property of properties) {
|
||||
if (!this._modifications.has(property.name))
|
||||
this._modifications.set(property.name, this.getPropertyFromRule(this._rules[0], property.name));
|
||||
if (!this._modifications.has(property.name)) {
|
||||
this._modifications.set(property.name,
|
||||
this.getPropertyFromRule(this._rules[0], property.name));
|
||||
}
|
||||
|
||||
if (property.value == "")
|
||||
if (property.value == "") {
|
||||
modifications.removeProperty(property.name);
|
||||
else
|
||||
} else {
|
||||
modifications.setProperty(property.name, property.value, "");
|
||||
}
|
||||
}
|
||||
|
||||
return modifications.apply().then(null, console.error);
|
||||
},
|
||||
@ -110,26 +114,33 @@ EditingSession.prototype = {
|
||||
let modifications = this._rules[0].startModifyingProperties();
|
||||
|
||||
for (let [property, value] of this._modifications) {
|
||||
if (value != "")
|
||||
if (value != "") {
|
||||
modifications.setProperty(property, value, "");
|
||||
else
|
||||
} else {
|
||||
modifications.removeProperty(property);
|
||||
}
|
||||
}
|
||||
|
||||
return modifications.apply().then(null, console.error);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this._doc = null;
|
||||
this._rules = null;
|
||||
this._modifications.clear();
|
||||
}
|
||||
};
|
||||
|
||||
function LayoutView(aInspector, aWindow)
|
||||
{
|
||||
this.inspector = aInspector;
|
||||
/**
|
||||
* The layout-view panel
|
||||
* @param {InspectorPanel} inspector An instance of the inspector-panel
|
||||
* currently loaded in the toolbox
|
||||
* @param {Window} win The window containing the panel
|
||||
*/
|
||||
function LayoutView(inspector, win) {
|
||||
this.inspector = inspector;
|
||||
|
||||
// <browser> is not always available (for Chrome targets for example)
|
||||
if (this.inspector.target.tab) {
|
||||
this.browser = aInspector.target.tab.linkedBrowser;
|
||||
}
|
||||
|
||||
this.doc = aWindow.document;
|
||||
this.doc = win.document;
|
||||
this.sizeLabel = this.doc.querySelector(".size > span");
|
||||
this.sizeHeadingLabel = this.doc.getElementById("element-size");
|
||||
|
||||
@ -137,13 +148,18 @@ function LayoutView(aInspector, aWindow)
|
||||
}
|
||||
|
||||
LayoutView.prototype = {
|
||||
init: function LV_init() {
|
||||
init: function() {
|
||||
this.update = this.update.bind(this);
|
||||
this.onNewNode = this.onNewNode.bind(this);
|
||||
|
||||
this.onNewSelection = this.onNewSelection.bind(this);
|
||||
this.inspector.selection.on("new-node-front", this.onNewSelection);
|
||||
|
||||
this.onNewNode = this.onNewNode.bind(this);
|
||||
this.inspector.sidebar.on("layoutview-selected", this.onNewNode);
|
||||
|
||||
this.onSidebarSelect = this.onSidebarSelect.bind(this);
|
||||
this.inspector.sidebar.on("select", this.onSidebarSelect);
|
||||
|
||||
// Store for the different dimensions of the node.
|
||||
// 'selector' refers to the element that holds the value in view.xhtml;
|
||||
// 'property' is what we are measuring;
|
||||
@ -206,7 +222,9 @@ LayoutView.prototype = {
|
||||
continue;
|
||||
|
||||
let dimension = this.map[i];
|
||||
editableItem({ element: this.doc.querySelector(dimension.selector) }, (element, event) => {
|
||||
editableItem({
|
||||
element: this.doc.querySelector(dimension.selector)
|
||||
}, (element, event) => {
|
||||
this.initEditor(element, event, dimension);
|
||||
});
|
||||
}
|
||||
@ -214,10 +232,39 @@ LayoutView.prototype = {
|
||||
this.onNewNode();
|
||||
},
|
||||
|
||||
/**
|
||||
* Start listening to reflows in the current tab.
|
||||
*/
|
||||
trackReflows: function() {
|
||||
if (!this.reflowFront) {
|
||||
let toolbox = this.inspector.toolbox;
|
||||
if (toolbox.target.form.reflowActor) {
|
||||
this.reflowFront = ReflowFront(toolbox.target.client, toolbox.target.form);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.reflowFront.on("reflows", this.update);
|
||||
this.reflowFront.start();
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop listening to reflows in the current tab.
|
||||
*/
|
||||
untrackReflows: function() {
|
||||
if (!this.reflowFront) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reflowFront.off("reflows", this.update);
|
||||
this.reflowFront.stop();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the user clicks on one of the editable values in the layoutview
|
||||
*/
|
||||
initEditor: function LV_initEditor(element, event, dimension) {
|
||||
initEditor: function(element, event, dimension) {
|
||||
let { property, realProperty } = dimension;
|
||||
if (!realProperty)
|
||||
realProperty = property;
|
||||
@ -233,26 +280,31 @@ LayoutView.prototype = {
|
||||
},
|
||||
|
||||
change: (value) => {
|
||||
if (NUMERIC.test(value))
|
||||
if (NUMERIC.test(value)) {
|
||||
value += "px";
|
||||
}
|
||||
|
||||
let properties = [
|
||||
{ name: property, value: value }
|
||||
]
|
||||
];
|
||||
|
||||
if (property.substring(0, 7) == "border-") {
|
||||
let bprop = property.substring(0, property.length - 5) + "style";
|
||||
let style = session.getProperty(bprop);
|
||||
if (!style || style == "none" || style == "hidden")
|
||||
if (!style || style == "none" || style == "hidden") {
|
||||
properties.push({ name: bprop, value: "solid" });
|
||||
}
|
||||
}
|
||||
|
||||
session.setProperties(properties);
|
||||
},
|
||||
|
||||
done: (value, commit) => {
|
||||
editor.elt.parentNode.classList.remove("editing");
|
||||
if (!commit)
|
||||
if (!commit) {
|
||||
session.revert();
|
||||
session.destroy();
|
||||
}
|
||||
}
|
||||
}, event);
|
||||
},
|
||||
@ -260,23 +312,35 @@ LayoutView.prototype = {
|
||||
/**
|
||||
* Is the layoutview visible in the sidebar?
|
||||
*/
|
||||
isActive: function LV_isActive() {
|
||||
return this.inspector.sidebar.getCurrentTabID() == "layoutview";
|
||||
isActive: function() {
|
||||
return this.inspector &&
|
||||
this.inspector.sidebar.getCurrentTabID() == "layoutview";
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy the nodes. Remove listeners.
|
||||
*/
|
||||
destroy: function LV_destroy() {
|
||||
destroy: function() {
|
||||
this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
|
||||
this.inspector.selection.off("new-node-front", this.onNewSelection);
|
||||
if (this.browser) {
|
||||
this.browser.removeEventListener("MozAfterPaint", this.update, true);
|
||||
}
|
||||
this.inspector.sidebar.off("select", this.onSidebarSelect);
|
||||
|
||||
this.sizeHeadingLabel = null;
|
||||
this.sizeLabel = null;
|
||||
this.inspector = null;
|
||||
this.doc = null;
|
||||
|
||||
if (this.reflowFront) {
|
||||
this.untrackReflows();
|
||||
this.reflowFront.destroy();
|
||||
this.reflowFront = null;
|
||||
}
|
||||
},
|
||||
|
||||
onSidebarSelect: function(e, sidebar) {
|
||||
if (sidebar !== "layoutview") {
|
||||
this.dim();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -287,7 +351,10 @@ LayoutView.prototype = {
|
||||
this.onNewNode().then(done, (err) => { console.error(err); done() });
|
||||
},
|
||||
|
||||
onNewNode: function LV_onNewNode() {
|
||||
/**
|
||||
* @return a promise that resolves when the view has been updated
|
||||
*/
|
||||
onNewNode: function() {
|
||||
if (this.isActive() &&
|
||||
this.inspector.selection.isConnected() &&
|
||||
this.inspector.selection.isElementNode()) {
|
||||
@ -295,41 +362,36 @@ LayoutView.prototype = {
|
||||
} else {
|
||||
this.dim();
|
||||
}
|
||||
|
||||
return this.update();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the layout boxes. No node are selected.
|
||||
* Hide the layout boxes and stop refreshing on reflows. No node is selected
|
||||
* or the layout-view sidebar is inactive.
|
||||
*/
|
||||
dim: function LV_dim() {
|
||||
if (this.browser) {
|
||||
this.browser.removeEventListener("MozAfterPaint", this.update, true);
|
||||
}
|
||||
this.trackingPaint = false;
|
||||
dim: function() {
|
||||
this.untrackReflows();
|
||||
this.doc.body.classList.add("dim");
|
||||
this.dimmed = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the layout boxes. A node is selected.
|
||||
* Show the layout boxes and start refreshing on reflows. A node is selected
|
||||
* and the layout-view side is active.
|
||||
*/
|
||||
undim: function LV_undim() {
|
||||
if (!this.trackingPaint) {
|
||||
if (this.browser) {
|
||||
this.browser.addEventListener("MozAfterPaint", this.update, true);
|
||||
}
|
||||
this.trackingPaint = true;
|
||||
}
|
||||
undim: function() {
|
||||
this.trackReflows();
|
||||
this.doc.body.classList.remove("dim");
|
||||
this.dimmed = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compute the dimensions of the node and update the values in
|
||||
* the layoutview/view.xhtml document. Returns a promise that will be resolved
|
||||
* when complete.
|
||||
* the layoutview/view.xhtml document.
|
||||
* @return a promise that will be resolved when complete.
|
||||
*/
|
||||
update: function LV_update() {
|
||||
update: function() {
|
||||
let lastRequest = Task.spawn((function*() {
|
||||
if (!this.isActive() ||
|
||||
!this.inspector.selection.isConnected() ||
|
||||
@ -415,6 +477,10 @@ LayoutView.prototype = {
|
||||
return this._lastRequest = lastRequest;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the box-model highlighter on the currently selected element
|
||||
* @param {Object} options Options passed to the highlighter actor
|
||||
*/
|
||||
showBoxModel: function(options={}) {
|
||||
let toolbox = this.inspector.toolbox;
|
||||
let nodeFront = this.inspector.selection.nodeFront;
|
||||
@ -422,6 +488,9 @@ LayoutView.prototype = {
|
||||
toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the box-model highlighter on the currently selected element
|
||||
*/
|
||||
hideBoxModel: function() {
|
||||
let toolbox = this.inspector.toolbox;
|
||||
|
||||
|
394
toolkit/devtools/server/actors/layout.js
Normal file
394
toolkit/devtools/server/actors/layout.js
Normal file
@ -0,0 +1,394 @@
|
||||
/* 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.
|
||||
*
|
||||
* - Observable: A utility parent class, meant at being extended by classes that
|
||||
* need a start/stop behavior.
|
||||
*
|
||||
* - 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.
|
||||
*
|
||||
* - 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, RetVal, types} = protocol;
|
||||
const events = require("sdk/event/core");
|
||||
const Heritage = require("sdk/core/heritage");
|
||||
const EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
|
||||
exports.register = function(handle) {
|
||||
handle.addGlobalActor(ReflowActor, "reflowActor");
|
||||
handle.addTabActor(ReflowActor, "reflowActor");
|
||||
};
|
||||
|
||||
exports.unregister = function(handle) {
|
||||
handle.removeGlobalActor(ReflowActor);
|
||||
handle.removeTabActor(ReflowActor);
|
||||
};
|
||||
|
||||
/**
|
||||
* The reflow actor tracks reflows and emits events about them.
|
||||
*/
|
||||
let 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});
|
||||
client.addActorPool(this);
|
||||
this.manage(this);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
protocol.Front.prototype.destroy.call(this);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Base class for all sorts of observers we need to create for a given window.
|
||||
* @param {TabActor} tabActor
|
||||
* @param {Function} callback Executed everytime the observer observes something
|
||||
*/
|
||||
function Observable(tabActor, callback) {
|
||||
this.tabActor = tabActor;
|
||||
this.win = tabActor.window;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
Observable.prototype = {
|
||||
/**
|
||||
* Is the observer currently observing
|
||||
*/
|
||||
observing: false,
|
||||
|
||||
/**
|
||||
* Start observing whatever it is this observer is supposed to observe
|
||||
*/
|
||||
start: function() {
|
||||
if (!this.observing) {
|
||||
this._start();
|
||||
this.observing = true;
|
||||
}
|
||||
},
|
||||
|
||||
_start: function() {
|
||||
/* To be implemented by sub-classes */
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop observing
|
||||
*/
|
||||
stop: function() {
|
||||
if (this.observing) {
|
||||
this._stop();
|
||||
this.observing = false;
|
||||
}
|
||||
},
|
||||
|
||||
_stop: function() {
|
||||
/* To be implemented by sub-classes */
|
||||
},
|
||||
|
||||
/**
|
||||
* To be called by sub-classes when something has been observed
|
||||
*/
|
||||
notifyCallback: function(...args) {
|
||||
this.observing && this.callback && this.callback.apply(null, args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop observing and detroy this observer instance
|
||||
*/
|
||||
destroy: function() {
|
||||
this.stop();
|
||||
this.callback = null;
|
||||
this.win = null;
|
||||
this.tabActor = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
*
|
||||
* @param {TabActor} tabActor
|
||||
*/
|
||||
function LayoutChangesObserver(tabActor) {
|
||||
Observable.call(this, tabActor);
|
||||
|
||||
this._startEventLoop = this._startEventLoop.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._onReflow = this._onReflow.bind(this);
|
||||
this.reflowObserver = new ReflowObserver(this.tabActor, this._onReflow);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, {
|
||||
/**
|
||||
* How long does this observer waits before emitting a batched reflows event.
|
||||
* 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.reflowObserver.destroy();
|
||||
this.reflows = null;
|
||||
|
||||
Observable.prototype.destroy.call(this);
|
||||
},
|
||||
|
||||
_start: function() {
|
||||
this.reflows = [];
|
||||
this._startEventLoop();
|
||||
this.reflowObserver.start();
|
||||
},
|
||||
|
||||
_stop: function() {
|
||||
this._stopEventLoop();
|
||||
this.reflows = [];
|
||||
this.reflowObserver.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() {
|
||||
// Send any reflows we have
|
||||
if (this.reflows && this.reflows.length) {
|
||||
this.emit("reflows", this.reflows);
|
||||
this.reflows = [];
|
||||
}
|
||||
this.eventLoopTimer = this.win.setTimeout(this._startEventLoop,
|
||||
this.EVENT_BATCHING_DELAY);
|
||||
},
|
||||
|
||||
_stopEventLoop: function() {
|
||||
this.win.clearTimeout(this.eventLoopTimer);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// 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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get a LayoutChangesObserver instance for a given window. This function makes
|
||||
* sure there is only one instance per window.
|
||||
* @param {TabActor} tabActor
|
||||
* @return {LayoutChangesObserver}
|
||||
*/
|
||||
let 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,
|
||||
refCounting: 1 // counting references allows to stop the observer when no
|
||||
// tabActor owns an instance
|
||||
});
|
||||
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;
|
||||
|
||||
/**
|
||||
* Instantiate and start a reflow observer on a given window's document element.
|
||||
* Will report any reflow that occurs in this window's docshell.
|
||||
* @extends Observable
|
||||
* @param {TabActor} tabActor
|
||||
* @param {Function} callback Executed everytime a reflow occurs
|
||||
*/
|
||||
function ReflowObserver(tabActor, callback) {
|
||||
Observable.call(this, tabActor, callback);
|
||||
this.docshell = this.win.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShell);
|
||||
}
|
||||
|
||||
ReflowObserver.prototype = Heritage.extend(Observable.prototype, {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
|
||||
Ci.nsISupportsWeakReference]),
|
||||
|
||||
_start: function() {
|
||||
this.docshell.addWeakReflowObserver(this);
|
||||
},
|
||||
|
||||
_stop: function() {
|
||||
this.docshell.removeWeakReflowObserver(this);
|
||||
},
|
||||
|
||||
reflow: function(start, end) {
|
||||
this.notifyCallback(start, end, false);
|
||||
},
|
||||
|
||||
reflowInterruptible: function(start, end) {
|
||||
this.notifyCallback(start, end, true);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
Observable.prototype.destroy.call(this);
|
||||
this.docshell = null;
|
||||
}
|
||||
});
|
@ -391,6 +391,7 @@ var DebuggerServer = {
|
||||
this.registerModule("devtools/server/actors/tracer");
|
||||
this.registerModule("devtools/server/actors/memory");
|
||||
this.registerModule("devtools/server/actors/eventlooplag");
|
||||
this.registerModule("devtools/server/actors/layout");
|
||||
if ("nsIProfiler" in Ci) {
|
||||
this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
|
||||
}
|
||||
|
@ -0,0 +1,243 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test the LayoutChangesObserver
|
||||
|
||||
let {
|
||||
getLayoutChangesObserver,
|
||||
releaseLayoutChangesObserver
|
||||
} = devtools.require("devtools/server/actors/layout");
|
||||
|
||||
// Mock the tabActor since we only really want to test the LayoutChangesObserver
|
||||
// and don't want to depend on a window object, nor want to test protocol.js
|
||||
function MockTabActor() {
|
||||
this.window = new MockWindow();
|
||||
}
|
||||
|
||||
function MockWindow() {}
|
||||
MockWindow.prototype = {
|
||||
QueryInterface: function() {
|
||||
let self = this;
|
||||
return {
|
||||
getInterface: function() {
|
||||
return {
|
||||
QueryInterface: function() {
|
||||
self.docShell = new MockDocShell();
|
||||
return self.docShell;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
setTimeout: function(cb) {
|
||||
// Simply return the cb itself so that we can execute it in the test instead
|
||||
// of depending on a real timeout
|
||||
return cb;
|
||||
},
|
||||
clearTimeout: function() {}
|
||||
};
|
||||
|
||||
function MockDocShell() {
|
||||
this.observer = null;
|
||||
}
|
||||
MockDocShell.prototype = {
|
||||
addWeakReflowObserver: function(observer) {
|
||||
this.observer = observer;
|
||||
},
|
||||
removeWeakReflowObserver: function(observer) {}
|
||||
};
|
||||
|
||||
function run_test() {
|
||||
instancesOfObserversAreSharedBetweenWindows();
|
||||
eventsAreBatched();
|
||||
noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts();
|
||||
observerIsAlreadyStarted();
|
||||
destroyStopsObserving();
|
||||
stoppingAndStartingSeveralTimesWorksCorrectly();
|
||||
reflowsArentStackedWhenStopped();
|
||||
stackedReflowsAreResetOnStop();
|
||||
}
|
||||
|
||||
function instancesOfObserversAreSharedBetweenWindows() {
|
||||
do_print("Checking that when requesting twice an instances of the observer " +
|
||||
"for the same TabActor, the instance is shared");
|
||||
|
||||
do_print("Checking 2 instances of the observer for the tabActor 1");
|
||||
let tabActor1 = new MockTabActor();
|
||||
let obs11 = getLayoutChangesObserver(tabActor1);
|
||||
let obs12 = getLayoutChangesObserver(tabActor1);
|
||||
do_check_eq(obs11, obs12);
|
||||
|
||||
do_print("Checking 2 instances of the observer for the tabActor 2");
|
||||
let tabActor2 = new MockTabActor();
|
||||
let obs21 = getLayoutChangesObserver(tabActor2);
|
||||
let obs22 = getLayoutChangesObserver(tabActor2);
|
||||
do_check_eq(obs21, obs22);
|
||||
|
||||
do_print("Checking that observers instances for 2 different tabActors are " +
|
||||
"different");
|
||||
do_check_neq(obs11, obs21);
|
||||
|
||||
releaseLayoutChangesObserver(tabActor1);
|
||||
releaseLayoutChangesObserver(tabActor1);
|
||||
releaseLayoutChangesObserver(tabActor2);
|
||||
releaseLayoutChangesObserver(tabActor2);
|
||||
}
|
||||
|
||||
function eventsAreBatched() {
|
||||
do_print("Checking that reflow events are batched and only sent when the " +
|
||||
"timeout expires");
|
||||
|
||||
// Note that in this test, we mock the TabActor and its window property, so we
|
||||
// also mock the setTimeout/clearTimeout mechanism and just call the callback
|
||||
// manually
|
||||
|
||||
let tabActor = new MockTabActor();
|
||||
let observer = getLayoutChangesObserver(tabActor);
|
||||
|
||||
let reflowsEvents = [];
|
||||
let onReflows = (event, reflows) => reflowsEvents.push(reflows);
|
||||
observer.on("reflows", onReflows);
|
||||
|
||||
do_print("Fake one reflow event");
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
do_print("Checking that no batched reflow event has been emitted");
|
||||
do_check_eq(reflowsEvents.length, 0);
|
||||
|
||||
do_print("Fake another reflow event");
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
do_print("Checking that still no batched reflow event has been emitted");
|
||||
do_check_eq(reflowsEvents.length, 0);
|
||||
|
||||
do_print("Faking timeout expiration and checking that reflow events are sent");
|
||||
observer.eventLoopTimer();
|
||||
do_check_eq(reflowsEvents.length, 1);
|
||||
do_check_eq(reflowsEvents[0].length, 2);
|
||||
|
||||
observer.off("reflows", onReflows);
|
||||
releaseLayoutChangesObserver(tabActor);
|
||||
}
|
||||
|
||||
function noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts() {
|
||||
do_print("Checking that if no reflows were detected and the event batching " +
|
||||
"loop expires, then no reflows event is sent");
|
||||
|
||||
let tabActor = new MockTabActor();
|
||||
let observer = getLayoutChangesObserver(tabActor);
|
||||
|
||||
let reflowsEvents = [];
|
||||
let onReflows = (event, reflows) => reflowsEvents.push(reflows);
|
||||
observer.on("reflows", onReflows);
|
||||
|
||||
do_print("Faking timeout expiration and checking for reflows");
|
||||
observer.eventLoopTimer();
|
||||
do_check_eq(reflowsEvents.length, 0);
|
||||
|
||||
observer.off("reflows", onReflows);
|
||||
releaseLayoutChangesObserver(tabActor);
|
||||
}
|
||||
|
||||
function observerIsAlreadyStarted() {
|
||||
do_print("Checking that the observer is already started when getting it");
|
||||
|
||||
let tabActor = new MockTabActor();
|
||||
let observer = getLayoutChangesObserver(tabActor);
|
||||
do_check_true(observer.observing);
|
||||
|
||||
observer.stop();
|
||||
do_check_false(observer.observing);
|
||||
|
||||
observer.start();
|
||||
do_check_true(observer.observing);
|
||||
|
||||
releaseLayoutChangesObserver(tabActor);
|
||||
}
|
||||
|
||||
function destroyStopsObserving() {
|
||||
do_print("Checking that the destroying the observer stops it");
|
||||
|
||||
let tabActor = new MockTabActor();
|
||||
let observer = getLayoutChangesObserver(tabActor);
|
||||
do_check_true(observer.observing);
|
||||
|
||||
observer.destroy();
|
||||
do_check_false(observer.observing);
|
||||
|
||||
releaseLayoutChangesObserver(tabActor);
|
||||
}
|
||||
|
||||
function stoppingAndStartingSeveralTimesWorksCorrectly() {
|
||||
do_print("Checking that the stopping and starting several times the observer" +
|
||||
" works correctly");
|
||||
|
||||
let tabActor = new MockTabActor();
|
||||
let observer = getLayoutChangesObserver(tabActor);
|
||||
|
||||
do_check_true(observer.observing);
|
||||
observer.start();
|
||||
observer.start();
|
||||
observer.start();
|
||||
do_check_true(observer.observing);
|
||||
|
||||
observer.stop();
|
||||
do_check_false(observer.observing);
|
||||
|
||||
observer.stop();
|
||||
observer.stop();
|
||||
do_check_false(observer.observing);
|
||||
|
||||
releaseLayoutChangesObserver(tabActor);
|
||||
}
|
||||
|
||||
function reflowsArentStackedWhenStopped() {
|
||||
do_print("Checking that when stopped, reflows aren't stacked in the observer");
|
||||
|
||||
let tabActor = new MockTabActor();
|
||||
let observer = getLayoutChangesObserver(tabActor);
|
||||
|
||||
do_print("Stoping the observer");
|
||||
observer.stop();
|
||||
|
||||
do_print("Faking reflows");
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
|
||||
do_print("Checking that reflows aren't recorded");
|
||||
do_check_eq(observer.reflows.length, 0);
|
||||
|
||||
do_print("Starting the observer and faking more reflows");
|
||||
observer.start();
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
|
||||
do_print("Checking that reflows are recorded");
|
||||
do_check_eq(observer.reflows.length, 3);
|
||||
|
||||
releaseLayoutChangesObserver(tabActor);
|
||||
}
|
||||
|
||||
function stackedReflowsAreResetOnStop() {
|
||||
do_print("Checking that stacked reflows are reset on stop");
|
||||
|
||||
let tabActor = new MockTabActor();
|
||||
let observer = getLayoutChangesObserver(tabActor);
|
||||
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
do_check_eq(observer.reflows.length, 1);
|
||||
|
||||
observer.stop();
|
||||
do_check_eq(observer.reflows.length, 0);
|
||||
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
do_check_eq(observer.reflows.length, 0);
|
||||
|
||||
observer.start();
|
||||
do_check_eq(observer.reflows.length, 0);
|
||||
|
||||
tabActor.window.docShell.observer.reflow();
|
||||
do_check_eq(observer.reflows.length, 1);
|
||||
|
||||
releaseLayoutChangesObserver(tabActor);
|
||||
}
|
@ -198,5 +198,6 @@ reason = bug 820380
|
||||
[test_ignore_caught_exceptions.js]
|
||||
[test_requestTypes.js]
|
||||
reason = bug 937197
|
||||
[test_layout-reflows-observer.js]
|
||||
[test_protocolSpec.js]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user