diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 6d8ddf2caca..44acd447db7 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1058,6 +1058,7 @@ pref("devtools.toolbox.sideEnabled", true);
pref("devtools.inspector.enabled", true);
pref("devtools.inspector.activeSidebar", "ruleview");
pref("devtools.inspector.markupPreview", false);
+pref("devtools.inspector.remote", false);
// Enable the Layout View
pref("devtools.layoutview.enabled", true);
diff --git a/browser/devtools/fontinspector/font-inspector.js b/browser/devtools/fontinspector/font-inspector.js
index 9983db21fd0..7c92217db8f 100644
--- a/browser/devtools/fontinspector/font-inspector.js
+++ b/browser/devtools/fontinspector/font-inspector.js
@@ -54,6 +54,7 @@ FontInspector.prototype = {
*/
onNewNode: function FI_onNewNode() {
if (this.isActive() &&
+ this.inspector.selection.isLocal() &&
this.inspector.selection.isConnected() &&
this.inspector.selection.isElementNode() &&
this.inspector.selection.reason != "highlighter") {
diff --git a/browser/devtools/framework/sidebar.js b/browser/devtools/framework/sidebar.js
index e312db4512d..af41f4d47ca 100644
--- a/browser/devtools/framework/sidebar.js
+++ b/browser/devtools/framework/sidebar.js
@@ -148,6 +148,11 @@ ToolSidebar.prototype = {
*/
handleEvent: function ToolSidebar_eventHandler(event) {
if (event.type == "select") {
+ if (this._currentTool == this.getCurrentTabID()) {
+ // Tool hasn't changed.
+ return;
+ }
+
let previousTool = this._currentTool;
this._currentTool = this.getCurrentTabID();
if (previousTool) {
diff --git a/browser/devtools/framework/target.js b/browser/devtools/framework/target.js
index d76ec7f9802..7a93b775046 100644
--- a/browser/devtools/framework/target.js
+++ b/browser/devtools/framework/target.js
@@ -15,6 +15,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
"resource://gre/modules/devtools/dbg-client.jsm");
+loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront);
+
const targets = new WeakMap();
const promiseTargets = new WeakMap();
@@ -250,6 +252,17 @@ TabTarget.prototype = {
return !!this._isThreadPaused;
},
+ get inspector() {
+ if (!this.form) {
+ throw new Error("Target.inspector requires an initialized remote actor.");
+ }
+ if (this._inspector) {
+ return this._inspector;
+ }
+ this._inspector = InspectorFront(this.client, this.form);
+ return this._inspector;
+ },
+
/**
* Adds remote protocol capabilities to the target, so that it can be used
* for tools that support the Remote Debugging Protocol even for local
diff --git a/browser/devtools/inspector/breadcrumbs.js b/browser/devtools/inspector/breadcrumbs.js
index 820ff6b7973..7b52b3e76ec 100644
--- a/browser/devtools/inspector/breadcrumbs.js
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -12,6 +12,9 @@ const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+let promise = require("sdk/core/promise");
const LOW_PRIORITY_ELEMENTS = {
"HEAD": true,
@@ -25,6 +28,18 @@ const LOW_PRIORITY_ELEMENTS = {
"TITLE": true,
};
+function resolveNextTick(value) {
+ let deferred = promise.defer();
+ Services.tm.mainThread.dispatch(() => {
+ try {
+ deferred.resolve(value);
+ } catch(ex) {
+ console.error(ex);
+ }
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ return deferred.promise;
+}
+
///////////////////////////////////////////////////////////////////////////
//// HTML Breadcrumbs
@@ -53,6 +68,8 @@ function HTMLBreadcrumbs(aInspector)
exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
HTMLBreadcrumbs.prototype = {
+ get walker() this.inspector.walker,
+
_init: function BC__init()
{
this.container = this.chromeDoc.getElementById("inspector-breadcrumbs");
@@ -83,12 +100,37 @@ HTMLBreadcrumbs.prototype = {
this.update = this.update.bind(this);
this.updateSelectors = this.updateSelectors.bind(this);
- this.selection.on("new-node", this.update);
+ this.selection.on("new-node-front", this.update);
this.selection.on("pseudoclass", this.updateSelectors);
this.selection.on("attribute-changed", this.updateSelectors);
this.update();
},
+ /**
+ * Include in a promise's then() chain to reject the chain
+ * when the breadcrumbs' selection has changed while the promise
+ * was outstanding.
+ */
+ selectionGuard: function() {
+ let selection = this.selection.nodeFront;
+ return (result) => {
+ if (selection != this.selection.nodeFront) {
+ return promise.reject("selection-changed");
+ }
+ return result;
+ }
+ },
+
+ /**
+ * Print any errors (except selection guard errors).
+ */
+ selectionGuardEnd: function(err) {
+ if (err != "selection-changed") {
+ console.error(err);
+ }
+ promise.reject(err);
+ },
+
/**
* Build a string that represents the node: tagName#id.class1.class2.
*
@@ -101,12 +143,19 @@ HTMLBreadcrumbs.prototype = {
if (aNode.id) {
text += "#" + aNode.id;
}
- for (let i = 0; i < aNode.classList.length; i++) {
- text += "." + aNode.classList[i];
+
+ if (aNode.className) {
+ let classList = aNode.className.split(/\s+/);
+ for (let i = 0; i < classList.length; i++) {
+ text += "." + classList[i];
+ }
}
+
+ // XXX: needs updating when pseudoclass-lock is remotable
+ let rawNode = aNode.rawNode();
for (let i = 0; i < PSEUDO_CLASSES.length; i++) {
let pseudo = PSEUDO_CLASSES[i];
- if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) {
+ if (DOMUtils.hasPseudoClassLock(rawNode, pseudo)) {
text += pseudo;
}
}
@@ -143,14 +192,20 @@ HTMLBreadcrumbs.prototype = {
tagLabel.textContent = aNode.tagName.toLowerCase();
idLabel.textContent = aNode.id ? ("#" + aNode.id) : "";
- let classesText = "";
- for (let i = 0; i < aNode.classList.length; i++) {
- classesText += "." + aNode.classList[i];
+ if (aNode.className) {
+ let classesText = "";
+ let classList = aNode.className.split(/\s+/);
+ for (let i = 0; i < classList.length; i++) {
+ classesText += "." + classList[i];
+ }
+ classesLabel.textContent = classesText;
}
- classesLabel.textContent = classesText;
+
+ // XXX: Until we have pseudoclass lock in the node.
+ let rawNode = aNode.rawNode();
let pseudos = PSEUDO_CLASSES.filter(function(pseudo) {
- return DOMUtils.hasPseudoClassLock(aNode, pseudo);
+ return DOMUtils.hasPseudoClassLock(rawNode, pseudo);
}, this);
pseudosLabel.textContent = pseudos.join("");
@@ -173,7 +228,7 @@ HTMLBreadcrumbs.prototype = {
// We make sure that the targeted node is selected
// because we want to use the nodemenu that only works
// for inspector.selection
- this.selection.setNode(aNode, "breadcrumbs");
+ this.selection.setNodeFront(aNode, "breadcrumbs");
let title = this.chromeDoc.createElement("menuitem");
title.setAttribute("label", this.inspector.strings.GetStringFromName("breadcrumbs.siblings"));
@@ -183,9 +238,11 @@ HTMLBreadcrumbs.prototype = {
let items = [title, separator];
- let nodes = aNode.parentNode.childNodes;
- for (let i = 0; i < nodes.length; i++) {
- if (nodes[i].nodeType == aNode.ELEMENT_NODE) {
+ this.walker.siblings(aNode, {
+ whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
+ }).then(siblings => {
+ let nodes = siblings.nodes;
+ for (let i = 0; i < nodes.length; i++) {
let item = this.chromeDoc.createElement("menuitem");
if (nodes[i] === aNode) {
item.setAttribute("disabled", "true");
@@ -198,14 +255,14 @@ HTMLBreadcrumbs.prototype = {
let selection = this.selection;
item.onmouseup = (function(aNode) {
return function() {
- selection.setNode(aNode, "breadcrumbs");
+ selection.setNodeFront(aNode, "breadcrumbs");
}
})(nodes[i]);
items.push(item);
+ this.inspector.showNodeMenu(aButton, "before_start", items);
}
- }
- this.inspector.showNodeMenu(aButton, "before_start", items);
+ });
},
/**
@@ -252,33 +309,40 @@ HTMLBreadcrumbs.prototype = {
if (event.type == "keypress" && this.selection.isElementNode()) {
let node = null;
- switch (event.keyCode) {
- case this.chromeWin.KeyEvent.DOM_VK_LEFT:
- if (this.currentIndex != 0) {
- node = this.nodeHierarchy[this.currentIndex - 1].node;
+
+
+ this._keyPromise = this._keyPromise || promise.resolve(null);
+
+ this._keyPromise = (this._keyPromise || promise.resolve(null)).then(() => {
+ switch (event.keyCode) {
+ case this.chromeWin.KeyEvent.DOM_VK_LEFT:
+ if (this.currentIndex != 0) {
+ node = promise.resolve(this.nodeHierarchy[this.currentIndex - 1].node);
+ }
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
+ if (this.currentIndex < this.nodeHierarchy.length - 1) {
+ node = promise.resolve(this.nodeHierarchy[this.currentIndex + 1].node);
+ }
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_UP:
+ node = this.walker.previousSibling(this.selection.nodeFront, {
+ whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
+ });
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_DOWN:
+ node = this.walker.nextSibling(this.selection.nodeFront, {
+ whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
+ });
+ break;
+ }
+
+ return node.then((node) => {
+ if (node) {
+ this.selection.setNodeFront(node, "breadcrumbs");
}
- break;
- case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
- if (this.currentIndex < this.nodeHierarchy.length - 1) {
- node = this.nodeHierarchy[this.currentIndex + 1].node;
- }
- break;
- case this.chromeWin.KeyEvent.DOM_VK_UP:
- node = this.selection.node.previousSibling;
- while (node && (node.nodeType != node.ELEMENT_NODE)) {
- node = node.previousSibling;
- }
- break;
- case this.chromeWin.KeyEvent.DOM_VK_DOWN:
- node = this.selection.node.nextSibling;
- while (node && (node.nodeType != node.ELEMENT_NODE)) {
- node = node.nextSibling;
- }
- break;
- }
- if (node) {
- this.selection.setNode(node, "breadcrumbs");
- }
+ });
+ });
event.preventDefault();
event.stopPropagation();
}
@@ -290,12 +354,18 @@ HTMLBreadcrumbs.prototype = {
destroy: function BC_destroy()
{
this.nodeHierarchy.forEach(function(crumb) {
- if (LayoutHelpers.isNodeConnected(crumb.node)) {
- DOMUtils.clearPseudoClassLocks(crumb.node);
+ // This node might have already been destroyed during
+ // shutdown. Will clean this up when pseudo-class lock
+ // is ported to the walker.
+ if (crumb.node.actorID) {
+ let rawNode = crumb.node.rawNode();
+ if (LayoutHelpers.isNodeConnected(rawNode)) {
+ DOMUtils.clearPseudoClassLocks(rawNode);
+ }
}
});
- this.selection.off("new-node", this.update);
+ this.selection.off("new-node-front", this.update);
this.selection.off("pseudoclass", this.updateSelectors);
this.selection.off("attribute-changed", this.updateSelectors);
@@ -401,7 +471,7 @@ HTMLBreadcrumbs.prototype = {
}
button.onBreadcrumbsClick = function onBreadcrumbsClick() {
- this.selection.setNode(aNode, "breadcrumbs");
+ this.selection.setNodeFront(aNode, "breadcrumbs");
}.bind(this);
button.onclick = (function _onBreadcrumbsRightClick(event) {
@@ -437,7 +507,7 @@ HTMLBreadcrumbs.prototype = {
fragment.insertBefore(button, lastButtonInserted);
lastButtonInserted = button;
this.nodeHierarchy.splice(originalLength, 0, {node: toAppend, button: button});
- toAppend = this.DOMHelpers.getParentObject(toAppend);
+ toAppend = toAppend.parentNode();
}
this.container.appendChild(fragment, this.container.firstChild);
},
@@ -451,24 +521,37 @@ HTMLBreadcrumbs.prototype = {
*/
getInterestingFirstNode: function BC_getInterestingFirstNode(aNode)
{
- let nextChild = this.DOMHelpers.getChildObject(aNode, 0);
- let fallback = null;
+ let deferred = promise.defer();
- while (nextChild) {
- if (nextChild.nodeType == aNode.ELEMENT_NODE) {
- if (!(nextChild.tagName in LOW_PRIORITY_ELEMENTS)) {
- return nextChild;
+ var fallback = null;
+
+ var moreChildren = () => {
+ this.walker.children(aNode, {
+ start: fallback,
+ maxNodes: 10,
+ whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
+ }).then(this.selectionGuard()).then(response => {
+ for (let node of response.nodes) {
+ if (!(node.tagName in LOW_PRIORITY_ELEMENTS)) {
+ deferred.resolve(node);
+ return;
+ }
+ if (!fallback) {
+ fallback = node;
+ }
}
- if (!fallback) {
- fallback = nextChild;
+ if (response.hasLast) {
+ deferred.resolve(fallback);
+ return;
+ } else {
+ moreChildren();
}
- }
- nextChild = this.DOMHelpers.getNextSibling(nextChild);
+ }).then(null, this.selectionGuardEnd);
}
- return fallback;
+ moreChildren();
+ return deferred.promise;
},
-
/**
* Find the "youngest" ancestor of a node which is already in the breadcrumbs.
*
@@ -483,7 +566,7 @@ HTMLBreadcrumbs.prototype = {
if (idx > -1) {
return idx;
} else {
- node = this.DOMHelpers.getParentObject(node);
+ node = node.parentNode();
}
}
return -1;
@@ -498,13 +581,16 @@ HTMLBreadcrumbs.prototype = {
// If the last displayed node is the selected node
if (this.currentIndex == this.nodeHierarchy.length - 1) {
let node = this.nodeHierarchy[this.currentIndex].node;
- let child = this.getInterestingFirstNode(node);
- // If the node has a child
- if (child) {
- // Show this child
- this.expand(child);
- }
+ return this.getInterestingFirstNode(node).then(child => {
+ // If the node has a child
+ if (child) {
+ // Show this child
+ this.expand(child);
+ }
+ });
}
+
+ return resolveNextTick(true);
},
/**
@@ -560,7 +646,7 @@ HTMLBreadcrumbs.prototype = {
return;
}
- let idx = this.indexOf(this.selection.node);
+ let idx = this.indexOf(this.selection.nodeFront);
// Is the node already displayed in the breadcrumbs?
if (idx > -1) {
@@ -571,24 +657,32 @@ HTMLBreadcrumbs.prototype = {
if (this.nodeHierarchy.length > 0) {
// No. We drop all the element that are not direct ancestors
// of the selection
- let parent = this.DOMHelpers.getParentObject(this.selection.node);
+ let parent = this.selection.nodeFront.parentNode();
let idx = this.getCommonAncestor(parent);
this.cutAfter(idx);
}
// we append the missing button between the end of the breadcrumbs display
// and the current node.
- this.expand(this.selection.node);
+ this.expand(this.selection.nodeFront);
// we select the current node button
- idx = this.indexOf(this.selection.node);
+ idx = this.indexOf(this.selection.nodeFront);
this.setCursor(idx);
}
- // Add the first child of the very last node of the breadcrumbs if possible.
- this.ensureFirstChild();
- this.updateSelectors();
- // Make sure the selected node and its neighbours are visible.
- this.scroll();
+ let doneUpdating = this.inspector.updating("breadcrumbs");
+ // Add the first child of the very last node of the breadcrumbs if possible.
+ this.ensureFirstChild().then(this.selectionGuard()).then(() => {
+ this.updateSelectors();
+
+ // Make sure the selected node and its neighbours are visible.
+ this.scroll();
+ this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
+ doneUpdating();
+ }).then(null, err => {
+ doneUpdating(this.selection.nodeFront);
+ this.selectionGuardEnd(err);
+ });
},
}
diff --git a/browser/devtools/inspector/highlighter.js b/browser/devtools/inspector/highlighter.js
index 753c882ebba..dfba4a03fac 100644
--- a/browser/devtools/inspector/highlighter.js
+++ b/browser/devtools/inspector/highlighter.js
@@ -228,6 +228,15 @@ Highlighter.prototype = {
*/
invalidateSize: function Highlighter_invalidateSize()
{
+ // The highlighter runs locally while the selection runs remotely,
+ // so we can't quite trust the selection's isConnected to protect us
+ // here, do the check manually.
+ if (!this.selection.node ||
+ !this.selection.node.ownerDocument ||
+ !this.selection.node.ownerDocument.defaultView) {
+ return;
+ }
+
let canHiglightNode = this.selection.isNode() &&
this.selection.isConnected() &&
this.selection.isElementNode();
diff --git a/browser/devtools/inspector/inspector-panel.js b/browser/devtools/inspector/inspector-panel.js
index 5d809cfa099..c7a36f38dc7 100644
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -13,7 +13,7 @@ let EventEmitter = require("devtools/shared/event-emitter");
let {CssLogic} = require("devtools/styleinspector/css-logic");
loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView);
-loader.lazyGetter(this, "Selection", () => require ("devtools/inspector/selection").Selection);
+loader.lazyGetter(this, "Selection", () => require("devtools/inspector/selection").Selection);
loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs);
loader.lazyGetter(this, "Highlighter", () => require("devtools/inspector/highlighter").Highlighter);
loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar);
@@ -44,6 +44,20 @@ InspectorPanel.prototype = {
* open is effectively an asynchronous constructor
*/
open: function InspectorPanel_open() {
+ return this.target.makeRemote().then(() => {
+ return this.target.inspector.getWalker();
+ }).then(walker => {
+ if (this._destroyPromise) {
+ walker.release().then(null, console.error);
+ }
+ this.walker = walker;
+ return this._getDefaultNodeForSelection();
+ }).then(defaultSelection => {
+ return this._deferredOpen(defaultSelection);
+ }).then(null, console.error);
+ },
+
+ _deferredOpen: function(defaultSelection) {
let deferred = Promise.defer();
this.onNavigatedAway = this.onNavigatedAway.bind(this);
@@ -57,13 +71,13 @@ InspectorPanel.prototype = {
this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
// Create an empty selection
- this._selection = new Selection();
+ this._selection = new Selection(this.walker);
this.onNewSelection = this.onNewSelection.bind(this);
- this.selection.on("new-node", this.onNewSelection);
+ this.selection.on("new-node-front", this.onNewSelection);
this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
- this.selection.on("before-new-node", this.onBeforeNewSelection);
+ this.selection.on("before-new-node-front", this.onBeforeNewSelection);
this.onDetached = this.onDetached.bind(this);
- this.selection.on("detached", this.onDetached);
+ this.selection.on("detached-front", this.onDetached);
this.breadcrumbs = new HTMLBreadcrumbs(this);
@@ -121,13 +135,7 @@ InspectorPanel.prototype = {
this.isReady = true;
// All the components are initialized. Let's select a node.
- if (this.target.isLocalTab) {
- this._selection.setNode(
- this._getDefaultNodeForSelection(this.browser.contentDocument));
- } else if (this.target.window) {
- this._selection.setNode(
- this._getDefaultNodeForSelection(this.target.window.document));
- }
+ this._selection.setNodeFront(defaultSelection);
if (this.highlighter) {
this.highlighter.unlock();
@@ -146,13 +154,17 @@ InspectorPanel.prototype = {
},
/**
- * Select node for default selection
+ * Return a promise that will resolve to the default node for selection.
*/
- _getDefaultNodeForSelection : function(document) {
+ _getDefaultNodeForSelection : function() {
// if available set body node as default selected node
// else set documentElement
- var defaultNode = document.body || document.documentElement;
- return defaultNode;
+ return this.walker.querySelector(this.walker.rootNode, "body").then(front => {
+ if (front) {
+ return front;
+ }
+ return this.walker.documentElement(this.walker.rootNode);
+ });
},
/**
@@ -262,35 +274,34 @@ InspectorPanel.prototype = {
*/
onNavigatedAway: function InspectorPanel_onNavigatedAway(event, payload) {
let newWindow = payload._navPayload || payload;
- this.selection.setNode(null);
+ this.walker.release().then(null, console.error);
+ this.walker = null;
+ this.selection.setNodeFront(null);
+ this.selection.setWalker(null);
this._destroyMarkup();
this.isDirty = false;
- let onDOMReady = function() {
- newWindow.removeEventListener("DOMContentLoaded", onDOMReady, true);
-
- if (this._destroyed) {
+ this.target.inspector.getWalker().then(walker => {
+ if (this._destroyPromise) {
+ walker.release().then(null, console.error);
return;
}
- if (!this.selection.node) {
- let defaultNode = this._getDefaultNodeForSelection(newWindow.document);
- this.selection.setNode(defaultNode, "navigateaway");
- }
- this._initMarkup();
+ this.walker = walker;
+ this.selection.setWalker(walker);
+ this._getDefaultNodeForSelection().then(defaultNode => {
+ if (this._destroyPromise) {
+ return;
+ }
+ this.selection.setNodeFront(defaultNode, "navigateaway");
- this.once("markuploaded", () => {
- this.markup.expandNode(this.selection.node);
+ this._initMarkup();
+ this.once("markuploaded", () => {
+ this.markup.expandNode(this.selection.node);
+ this.setupSearchBox();
+ });
});
-
- this.setupSearchBox();
- }.bind(this);
-
- if (newWindow.document.readyState == "loading") {
- newWindow.addEventListener("DOMContentLoaded", onDOMReady, true);
- } else {
- onDOMReady();
- }
+ });
},
/**
@@ -298,6 +309,69 @@ InspectorPanel.prototype = {
*/
onNewSelection: function InspectorPanel_onNewSelection() {
this.cancelLayoutChange();
+
+ // Wait for all the known tools to finish updating and then let the
+ // client know.
+ let selection = this.selection.nodeFront;
+ let selfUpdate = this.updating("inspector-panel");
+ Services.tm.mainThread.dispatch(() => {
+ try {
+ selfUpdate(selection);
+ } catch(ex) {
+ console.error(ex);
+ }
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ /**
+ * Delay the "inspector-updated" notification while a tool
+ * is updating itself. Returns a function that must be
+ * invoked when the tool is done updating with the node
+ * that the tool is viewing.
+ */
+ updating: function(name) {
+ if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
+ this.cancelUpdate();
+ }
+
+ if (!this._updateProgress) {
+ // Start an update in progress.
+ var self = this;
+ this._updateProgress = {
+ node: this.selection.nodeFront,
+ outstanding: new Set(),
+ checkDone: function() {
+ if (this !== self._updateProgress) {
+ return;
+ }
+ if (this.node !== self.selection.nodeFront) {
+ self.cancelUpdate();
+ return;
+ }
+ if (this.outstanding.size !== 0) {
+ return;
+ }
+
+ self._updateProgress = null;
+ self.emit("inspector-updated");
+ },
+ }
+ }
+
+ let progress = this._updateProgress;
+ let done = function() {
+ progress.outstanding.delete(done);
+ progress.checkDone();
+ };
+ progress.outstanding.add(done);
+ return done;
+ },
+
+ /**
+ * Cancel notification of inspector updates.
+ */
+ cancelUpdate: function() {
+ this._updateProgress = null;
},
/**
@@ -317,18 +391,24 @@ InspectorPanel.prototype = {
onDetached: function InspectorPanel_onDetached(event, parentNode) {
this.cancelLayoutChange();
this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
- this.selection.setNode(parentNode, "detached");
+ this.selection.setNodeFront(parentNode, "detached");
},
/**
* Destroy the inspector.
*/
destroy: function InspectorPanel__destroy() {
- if (this._destroyed) {
- return Promise.resolve(null);
+ if (this._destroyPromise) {
+ return this._destroyPromise;
+ }
+ if (this.walker) {
+ this._destroyPromise = this.walker.release().then(null, console.error);
+ delete this.walker;
+ } else {
+ this._destroyPromise = Promise.resolve(null);
}
- this._destroyed = true;
+ this.cancelUpdate();
this.cancelLayoutChange();
if (this.browser) {
@@ -358,9 +438,10 @@ InspectorPanel.prototype = {
this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
this.breadcrumbs.destroy();
this.searchSuggestions.destroy();
- this.selection.off("new-node", this.onNewSelection);
+ this.selection.off("new-node-front", this.onNewSelection);
this.selection.off("before-new-node", this.onBeforeNewSelection);
- this.selection.off("detached", this.onDetached);
+ this.selection.off("before-new-node-front", this.onBeforeNewSelection);
+ this.selection.off("detached-front", this.onDetached);
this._destroyMarkup();
this._selection.destroy();
this._selection = null;
@@ -374,7 +455,7 @@ InspectorPanel.prototype = {
this.nodemenu = null;
this.highlighter = null;
- return Promise.resolve(null);
+ return this._destroyPromise;
},
/**
@@ -501,7 +582,7 @@ InspectorPanel.prototype = {
if (this.selection.isElementNode()) {
if (DOMUtils.hasPseudoClassLock(this.selection.node, aPseudo)) {
this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
- DOMUtils.removePseudoClassLock(crumb.node, aPseudo);
+ DOMUtils.removePseudoClassLock(crumb.node.rawNode(), aPseudo);
});
} else {
let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
@@ -522,7 +603,7 @@ InspectorPanel.prototype = {
clearPseudoClasses: function InspectorPanel_clearPseudoClasses() {
this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
try {
- DOMUtils.clearPseudoClassLocks(crumb.node);
+ DOMUtils.clearPseudoClassLocks(crumb.node.rawNode());
} catch(e) {
// Ignore dead nodes after navigation.
}
@@ -534,6 +615,9 @@ InspectorPanel.prototype = {
*/
toggleHighlighter: function InspectorPanel_toggleHighlighter(event)
{
+ if (!this.highlighter) {
+ return;
+ }
if (event.type == "mouseover") {
this.highlighter.hide();
}
diff --git a/browser/devtools/inspector/selection.js b/browser/devtools/inspector/selection.js
index 6963f6fe362..a7364fab8d2 100644
--- a/browser/devtools/inspector/selection.js
+++ b/browser/devtools/inspector/selection.js
@@ -4,13 +4,13 @@
* 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/. */
-const {Cu} = require("chrome");
+const {Cu, Ci} = require("chrome");
let EventEmitter = require("devtools/shared/event-emitter");
/**
* API
*
- * new Selection(node=null, track={attributes,detached});
+ * new Selection(walker=null, node=null, track={attributes,detached});
* destroy()
* node (readonly)
* setNode(node, origin="unknown")
@@ -53,19 +53,22 @@ let EventEmitter = require("devtools/shared/event-emitter");
* @param node Inner node.
* Can be null. Can be (un)set in the future via the "node" property;
* @param trackAttribute Tell if events should be fired when the attributes of
- * the ndoe change.
+ * the node change.
*
*/
-function Selection(node=null, track={attributes:true,detached:true}) {
+function Selection(walker, node=null, track={attributes:true,detached:true}) {
EventEmitter.decorate(this);
+
this._onMutations = this._onMutations.bind(this);
this.track = track;
+ this.setWalker(walker);
this.setNode(node);
}
exports.Selection = Selection;
Selection.prototype = {
+ _walker: null,
_node: null,
_onMutations: function(mutations) {
@@ -86,60 +89,41 @@ Selection.prototype = {
if (attributeChange)
this.emit("attribute-changed");
- if (detached)
- this.emit("detached", parentNode);
- },
-
- _attachEvents: function SN__attachEvents() {
- if (!this.window || !this.isNode() || !this.track) {
- return;
+ if (detached) {
+ this.emit("detached", parentNode ? parentNode.rawNode() : null);
+ this.emit("detached-front", parentNode);
}
-
- if (this.track.attributes) {
- this._nodeObserver = new this.window.MutationObserver(this._onMutations);
- this._nodeObserver.observe(this.node, {attributes: true});
- }
-
- if (this.track.detached) {
- this._docObserver = new this.window.MutationObserver(this._onMutations);
- this._docObserver.observe(this.document.documentElement, {childList: true, subtree: true});
- }
- },
-
- _detachEvents: function SN__detachEvents() {
- // `disconnect` fail if node's document has
- // been deleted.
- try {
- if (this._nodeObserver)
- this._nodeObserver.disconnect();
- } catch(e) {}
- try {
- if (this._docObserver)
- this._docObserver.disconnect();
- } catch(e) {}
},
destroy: function SN_destroy() {
- this._detachEvents();
this.setNode(null);
+ this.setWalker(null);
},
- setNode: function SN_setNode(value, reason="unknown") {
- this.reason = reason;
- if (value !== this._node) {
- this.emit("before-new-node", value, reason);
- let previousNode = this._node;
- this._detachEvents();
- this._node = value;
- this._attachEvents();
- this.emit("new-node", previousNode, this.reason);
+ setWalker: function(walker) {
+ if (this._walker) {
+ this._walker.off("mutations", this._onMutations);
+ }
+ this._walker = walker;
+ if (this._walker) {
+ this._walker.on("mutations", this._onMutations);
}
},
+ // Not remote-safe
+ setNode: function SN_setNode(value, reason="unknown") {
+ if (value) {
+ value = this._walker.frontForRawNode(value);
+ }
+ this.setNodeFront(value, reason);
+ },
+
+ // Not remote-safe
get node() {
return this._node;
},
+ // Not remote-safe
get window() {
if (this.isNode()) {
return this.node.ownerDocument.defaultView;
@@ -147,6 +131,7 @@ Selection.prototype = {
return null;
},
+ // Not remote-safe
get document() {
if (this.isNode()) {
return this.node.ownerDocument;
@@ -154,28 +139,79 @@ Selection.prototype = {
return null;
},
+ setNodeFront: function(value, reason="unknown") {
+ this.reason = reason;
+ if (value !== this._nodeFront) {
+ let rawValue = value ? value.rawNode() : value;
+ this.emit("before-new-node", rawValue, reason);
+ this.emit("before-new-node-front", value, reason);
+ let previousNode = this._node;
+ let previousFront = this._nodeFront;
+ this._node = rawValue;
+ this._nodeFront = value;
+ this.emit("new-node", previousNode, this.reason);
+ this.emit("new-node-front", value, this.reason);
+ }
+ },
+
+ get documentFront() {
+ return this._walker.document(this._nodeFront);
+ },
+
+ get nodeFront() {
+ return this._nodeFront;
+ },
+
isRoot: function SN_isRootNode() {
return this.isNode() &&
this.isConnected() &&
- this.node.ownerDocument.documentElement === this.node;
+ this._nodeFront.isDocumentElement;
},
isNode: function SN_isNode() {
- return (this.node &&
- !Cu.isDeadWrapper(this.node) &&
- this.node.ownerDocument &&
- this.node.ownerDocument.defaultView &&
- this.node instanceof this.node.ownerDocument.defaultView.Node);
+ if (!this._nodeFront) {
+ return false;
+ }
+
+ // As long as tools are still accessing node.rawNode(),
+ // this needs to stay here.
+ if (this._node && Cu.isDeadWrapper(this._node)) {
+ return false;
+ }
+
+ return true;
+ },
+
+ isLocal: function SN_nsLocal() {
+ return !!this._node;
},
isConnected: function SN_isConnected() {
- try {
- let doc = this.document;
- return doc && doc.defaultView && doc.documentElement.contains(this.node);
- } catch (e) {
- // "can't access dead object" error
+ let node = this._nodeFront;
+ if (!node || !node.actorID) {
return false;
}
+
+ // As long as there are still tools going around
+ // accessing node.rawNode, this needs to stay.
+ let rawNode = node.rawNode();
+ if (rawNode) {
+ try {
+ let doc = this.document;
+ return (doc && doc.defaultView && doc.documentElement.contains(rawNode));
+ } catch (e) {
+ // "can't access dead object" error
+ return false;
+ }
+ }
+
+ while(node) {
+ if (node === this._walker.rootNode) {
+ return true;
+ }
+ node = node.parentNode();
+ };
+ return false;
},
isHTMLNode: function SN_isHTMLNode() {
@@ -186,50 +222,50 @@ Selection.prototype = {
// Node type
isElementNode: function SN_isElementNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.ELEMENT_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE;
},
isAttributeNode: function SN_isAttributeNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.ATTRIBUTE_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ATTRIBUTE_NODE;
},
isTextNode: function SN_isTextNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.TEXT_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.TEXT_NODE;
},
isCDATANode: function SN_isCDATANode() {
- return this.isNode() && this.node.nodeType == this.window.Node.CDATA_SECTION_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.CDATA_SECTION_NODE;
},
isEntityRefNode: function SN_isEntityRefNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.ENTITY_REFERENCE_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_REFERENCE_NODE;
},
isEntityNode: function SN_isEntityNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.ENTITY_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_NODE;
},
isProcessingInstructionNode: function SN_isProcessingInstructionNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.PROCESSING_INSTRUCTION_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE;
},
isCommentNode: function SN_isCommentNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.PROCESSING_INSTRUCTION_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE;
},
isDocumentNode: function SN_isDocumentNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.DOCUMENT_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE;
},
isDocumentTypeNode: function SN_isDocumentTypeNode() {
- return this.isNode() && this.node.nodeType ==this.window. Node.DOCUMENT_TYPE_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE;
},
isDocumentFragmentNode: function SN_isDocumentFragmentNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.DOCUMENT_FRAGMENT_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE;
},
isNotationNode: function SN_isNotationNode() {
- return this.isNode() && this.node.nodeType == this.window.Node.NOTATION_NODE;
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.NOTATION_NODE;
},
}
diff --git a/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js b/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js
index a6d1b2c3158..d2b98a7c4d3 100644
--- a/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js
+++ b/browser/devtools/inspector/test/browser_inspector_breadcrumbs.js
@@ -44,7 +44,7 @@ function test()
{
inspector = aInspector;
cursor = 0;
- inspector.selection.on("new-node", nodeSelected);
+ inspector.on("breadcrumbs-updated", nodeSelected);
executeSoon(function() {
inspector.selection.setNode(nodes[0].node);
});
@@ -52,17 +52,16 @@ function test()
function nodeSelected()
{
- executeSoon(function() {
- performTest();
- cursor++;
- if (cursor >= nodes.length) {
- inspector.selection.off("new-node", nodeSelected);
- finishUp();
- } else {
- let node = nodes[cursor].node;
- inspector.selection.setNode(node);
- }
- });
+ performTest();
+ cursor++;
+
+ if (cursor >= nodes.length) {
+ inspector.off("breadcrumbs-updated", nodeSelected);
+ finishUp();
+ } else {
+ let node = nodes[cursor].node;
+ inspector.selection.setNode(node);
+ }
}
function performTest()
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js b/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js
index 79ba64580ff..c67db1e87d0 100644
--- a/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_672902_keyboard_shortcuts.js
@@ -34,11 +34,12 @@ function test()
{
inspector = aInspector;
- executeSoon(function() {
- inspector.selection.once("new-node", highlightHeaderNode);
- // Test that navigating around without a selected node gets us to the
- // head element.
- node = doc.querySelector("h1");
+ // Make sure the body element is selected initially.
+ node = doc.querySelector("body");
+ inspector.once("inspector-updated", () => {
+ is(inspector.selection.node, node, "Body should be selected initially.");
+ node = doc.querySelector("h1")
+ inspector.once("inspector-updated", highlightHeaderNode);
let bc = inspector.breadcrumbs;
bc.nodeHierarchy[bc.currentIndex].button.focus();
EventUtils.synthesizeKey("VK_RIGHT", { });
@@ -50,7 +51,7 @@ function test()
is(inspector.selection.node, node, "selected h1 element");
executeSoon(function() {
- inspector.selection.once("new-node", highlightParagraphNode);
+ inspector.once("inspector-updated", highlightParagraphNode);
// Test that moving to the next sibling works.
node = doc.querySelector("p");
EventUtils.synthesizeKey("VK_DOWN", { });
@@ -62,7 +63,7 @@ function test()
is(inspector.selection.node, node, "selected p element");
executeSoon(function() {
- inspector.selection.once("new-node", highlightHeaderNodeAgain);
+ inspector.once("inspector-updated", highlightHeaderNodeAgain);
// Test that moving to the previous sibling works.
node = doc.querySelector("h1");
EventUtils.synthesizeKey("VK_UP", { });
@@ -74,7 +75,7 @@ function test()
is(inspector.selection.node, node, "selected h1 element");
executeSoon(function() {
- inspector.selection.once("new-node", highlightParentNode);
+ inspector.once("inspector-updated", highlightParentNode);
// Test that moving to the parent works.
node = doc.querySelector("body");
EventUtils.synthesizeKey("VK_LEFT", { });
diff --git a/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js b/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
index da286979bb1..b500be2b543 100644
--- a/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
@@ -34,9 +34,10 @@ function test()
let tmp = {};
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tmp);
ok(!tmp.LayoutHelpers.isNodeConnected(node), "Node considered as disconnected.");
- executeSoon(function() {
- is(inspector.selection.node, parentNode, "parent of selection got selected");
+ // Wait for the selection to process the mutation
+ inspector.walker.on("mutations", () => {
+ is(inspector.selection.node, parentNode, "parent of selection got selected");
finishUp();
});
}
diff --git a/browser/devtools/inspector/test/browser_inspector_initialization.js b/browser/devtools/inspector/test/browser_inspector_initialization.js
index 56e6ec493c5..66f93c38d58 100644
--- a/browser/devtools/inspector/test/browser_inspector_initialization.js
+++ b/browser/devtools/inspector/test/browser_inspector_initialization.js
@@ -40,27 +40,29 @@ function startInspectorTests(toolbox)
let p = doc.querySelector("p");
inspector.selection.setNode(p);
+ inspector.once("inspector-updated", () => {
+ testHighlighter(p);
+ testMarkupView(p);
+ testBreadcrumbs(p);
- testHighlighter(p);
- testMarkupView(p);
- testBreadcrumbs(p);
+ let span = doc.querySelector("span");
+ span.scrollIntoView();
- let span = doc.querySelector("span");
- span.scrollIntoView();
+ inspector.selection.setNode(span);
+ inspector.once("inspector-updated", () => {
+ testHighlighter(span);
+ testMarkupView(span);
+ testBreadcrumbs(span);
- inspector.selection.setNode(span);
-
- testHighlighter(span);
- testMarkupView(span);
- testBreadcrumbs(span);
-
- toolbox.once("destroyed", function() {
- ok("true", "'destroyed' notification received.");
- let target = TargetFactory.forTab(gBrowser.selectedTab);
- ok(!gDevTools.getToolbox(target), "Toolbox destroyed.");
- executeSoon(runContextMenuTest);
+ toolbox.once("destroyed", function() {
+ ok("true", "'destroyed' notification received.");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ ok(!gDevTools.getToolbox(target), "Toolbox destroyed.");
+ executeSoon(runContextMenuTest);
+ });
+ toolbox.destroy();
+ });
});
- toolbox.destroy();
}
@@ -79,7 +81,7 @@ function testMarkupView(node)
function testBreadcrumbs(node)
{
let b = getActiveInspector().breadcrumbs;
- let expectedText = b.prettyPrintNodeAsText(node);
+ let expectedText = b.prettyPrintNodeAsText(getNodeFront(node));
let button = b.container.querySelector("button[checked=true]");
ok(button, "A crumbs is checked=true");
is(button.getAttribute("tooltiptext"), expectedText, "Crumb refers to the right node");
diff --git a/browser/devtools/inspector/test/browser_inspector_menu.js b/browser/devtools/inspector/test/browser_inspector_menu.js
index 8e257b1b80e..378cb2f9945 100644
--- a/browser/devtools/inspector/test/browser_inspector_menu.js
+++ b/browser/devtools/inspector/test/browser_inspector_menu.js
@@ -31,21 +31,23 @@ function test() {
function checkDocTypeMenuItems() {
info("Checking context menu entries for doctype node");
inspector.selection.setNode(doc.doctype);
- let docTypeNode = getMarkupTagNodeContaining("");
+ inspector.once("inspector-updated", () => {
+ let docTypeNode = getMarkupTagNodeContaining("");
- // Right-click doctype tag
- contextMenuClick(docTypeNode);
+ // Right-click doctype tag
+ contextMenuClick(docTypeNode);
- checkDisabled("node-menu-copyinner");
- checkDisabled("node-menu-copyouter");
- checkDisabled("node-menu-copyuniqueselector");
- checkDisabled("node-menu-delete");
+ checkDisabled("node-menu-copyinner");
+ checkDisabled("node-menu-copyouter");
+ checkDisabled("node-menu-copyuniqueselector");
+ checkDisabled("node-menu-delete");
- for (let name of ["hover", "active", "focus"]) {
- checkDisabled("node-menu-pseudo-" + name);
- }
+ for (let name of ["hover", "active", "focus"]) {
+ checkDisabled("node-menu-pseudo-" + name);
+ }
- checkElementMenuItems();
+ checkElementMenuItems();
+ });
}
function checkElementMenuItems() {
diff --git a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
index b0d9e17fc95..623c73b0bc0 100644
--- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
@@ -51,11 +51,11 @@ function createDocument()
function selectNode(aInspector)
{
inspector = aInspector;
- inspector.selection.setNode(div);
inspector.sidebar.once("ruleview-ready", function() {
ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
inspector.sidebar.select("ruleview");
- performTests();
+ inspector.selection.setNode(div);
+ inspector.once("inspector-updated", performTests);
});
}
@@ -75,29 +75,37 @@ function performTests()
// toggle it back on
inspector.togglePseudoClass(pseudo);
- testNavigate();
-
- // close the inspector
- finishUp();
+ testNavigate(() => {
+ // close the inspector
+ finishUp();
+ });
}
-function testNavigate()
+function testNavigate(callback)
{
inspector.selection.setNode(parentDiv);
+ inspector.once("inspector-updated", () => {
- // make sure it's still on after naving to parent
- is(DOMUtils.hasPseudoClassLock(div, pseudo), true,
- "pseudo-class lock is still applied after inspecting ancestor");
+ // make sure it's still on after naving to parent
+ is(DOMUtils.hasPseudoClassLock(div, pseudo), true,
+ "pseudo-class lock is still applied after inspecting ancestor");
- inspector.selection.setNode(div2);
+ inspector.selection.setNode(div2);
- // make sure it's removed after naving to a non-hierarchy node
- is(DOMUtils.hasPseudoClassLock(div, pseudo), false,
- "pseudo-class lock is removed after inspecting sibling node");
+ inspector.once("inspector-updated", () => {
- // toggle it back on
- inspector.selection.setNode(div);
- inspector.togglePseudoClass(pseudo);
+ // make sure it's removed after naving to a non-hierarchy node
+ is(DOMUtils.hasPseudoClassLock(div, pseudo), false,
+ "pseudo-class lock is removed after inspecting sibling node");
+
+ // toggle it back on
+ inspector.selection.setNode(div);
+ inspector.once("inspector-updated", () => {
+ inspector.togglePseudoClass(pseudo);
+ callback();
+ });
+ });
+ });
}
function testAdded()
diff --git a/browser/devtools/inspector/test/head.js b/browser/devtools/inspector/test/head.js
index e5b84ee0c51..62894973ee3 100644
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -5,6 +5,13 @@
const Cu = Components.utils;
const Ci = Components.interfaces;
const Cc = Components.classes;
+
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+
let tempScope = {};
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope);
let LayoutHelpers = tempScope.LayoutHelpers;
@@ -19,6 +26,12 @@ let console = tempScope.console;
let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
+SimpleTest.registerCleanupFunction(() => {
+ console.error("Here we are\n")
+ let {DebuggerServer} = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+ console.error("DebuggerServer open connections: " + Object.getOwnPropertyNames(DebuggerServer._connections).length);
+});
+
function openInspector(callback)
{
let target = TargetFactory.forTab(gBrowser.selectedTab);
@@ -33,6 +46,12 @@ function getActiveInspector()
return gDevTools.getToolbox(target).getPanel("inspector");
}
+function getNodeFront(node)
+{
+ let inspector = getActiveInspector();
+ return inspector.walker.frontForRawNode(node);
+}
+
function isHighlighting()
{
let outline = getActiveInspector().highlighter.outline;
diff --git a/browser/devtools/markupview/test/browser_inspector_markup_edit.js b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
index 89af699149e..2a88bf1c82e 100644
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.js
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
@@ -307,8 +307,11 @@ function test() {
id: "node18",
});
- is(inspector.highlighter.nodeInfo.classesBox.textContent, "",
- "No classes in the infobar before edit.");
+ /**
+ * XXX: disabled until the remote markup view is enabled
+ * is(inspector.highlighter.nodeInfo.classesBox.textContent, "",
+ * "No classes in the infobar before edit.");
+ */
},
execute: function(after) {
inspector.once("markupmutation", function() {
@@ -326,8 +329,12 @@ function test() {
class: "newclass",
style: "color:green"
});
- is(inspector.highlighter.nodeInfo.classesBox.textContent, ".newclass",
- "Correct classes in the infobar after edit.");
+
+ /**
+ * XXX: disabled until the remote markup view is enabled
+ *is(inspector.highlighter.nodeInfo.classesBox.textContent, ".newclass",
+ * "Correct classes in the infobar after edit.");
+ */
}
};
testAsyncSetup(test, editTagName);
diff --git a/browser/devtools/netmonitor/netmonitor-controller.js b/browser/devtools/netmonitor/netmonitor-controller.js
index 72e7a8a7056..1da4336b5c5 100644
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -250,6 +250,7 @@ TargetEventsHandler.prototype = {
case "will-navigate": {
// Reset UI.
NetMonitorView.RequestsMenu.reset();
+ NetMonitorView.Sidebar.reset();
NetMonitorView.NetworkDetails.reset();
// Reset global helpers cache.
diff --git a/browser/devtools/netmonitor/netmonitor-view.js b/browser/devtools/netmonitor/netmonitor-view.js
index b8437dc6b57..afdd1e59fc4 100644
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -165,7 +165,7 @@ let NetMonitorView = {
}
if (aTabIndex !== undefined) {
- $("#details-pane").selectedIndex = aTabIndex;
+ $("#event-details-pane").selectedIndex = aTabIndex;
}
},
@@ -351,8 +351,63 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
this.refreshSummary();
this.refreshZebra();
+
+ if (aId == this._preferredItemId) {
+ this.selectedItem = requestItem;
+ }
},
+ /**
+ * Create a new custom request form populated with the data from
+ * the currently selected request.
+ */
+ cloneSelectedRequest: function() {
+ let selected = this.selectedItem.attachment;
+
+ // Create the element node for the network request item.
+ let menuView = this._createMenuView(selected.method, selected.url);
+
+ let newItem = this.push([menuView], {
+ attachment: Object.create(selected, {
+ isCustom: { value: true }
+ })
+ });
+
+ // Immediately switch to new request pane.
+ this.selectedItem = newItem;
+ },
+
+ /**
+ * Send a new HTTP request using the data in the custom request form.
+ */
+ sendCustomRequest: function() {
+ let selected = this.selectedItem.attachment;
+
+ let data = Object.create(selected, {
+ headers: { value: selected.requestHeaders.headers }
+ });
+
+ if (selected.requestPostData) {
+ data.body = selected.requestPostData.postData.text;
+ }
+
+ NetMonitorController.webConsoleClient.sendHTTPRequest(data, (response) => {
+ let id = response.eventActor.actor;
+ this._preferredItemId = id;
+ });
+
+ this.closeCustomRequest();
+ },
+
+ /**
+ * Remove the currently selected custom request.
+ */
+ closeCustomRequest: function() {
+ this.remove(this.selectedItem);
+
+ NetMonitorView.Sidebar.toggle(false);
+ },
+
/**
* Filters all network requests in this container by a specified type.
*
@@ -690,11 +745,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
case "status":
requestItem.attachment.status = value;
- this._updateMenuView(requestItem, key, value);
+ this.updateMenuView(requestItem, key, value);
break;
case "statusText":
requestItem.attachment.statusText = value;
- this._updateMenuView(requestItem, key,
+ this.updateMenuView(requestItem, key,
requestItem.attachment.status + " " +
requestItem.attachment.statusText);
break;
@@ -703,11 +758,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
case "contentSize":
requestItem.attachment.contentSize = value;
- this._updateMenuView(requestItem, key, value);
+ this.updateMenuView(requestItem, key, value);
break;
case "mimeType":
requestItem.attachment.mimeType = value;
- this._updateMenuView(requestItem, key, value);
+ this.updateMenuView(requestItem, key, value);
break;
case "responseContent":
requestItem.attachment.responseContent = value;
@@ -715,7 +770,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
case "totalTime":
requestItem.attachment.totalTime = value;
requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value;
- this._updateMenuView(requestItem, key, value);
+ this.updateMenuView(requestItem, key, value);
this._registerLastRequestEnd(requestItem.attachment.endedMillis);
break;
case "eventTimings":
@@ -757,23 +812,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
* The network request view.
*/
_createMenuView: function(aMethod, aUrl) {
- let uri = nsIURL(aUrl);
- let nameWithQuery = this._getUriNameWithQuery(uri);
- let hostPort = this._getUriHostPort(uri);
-
let template = $("#requests-menu-item-template");
let fragment = document.createDocumentFragment();
- let method = $(".requests-menu-method", template);
- method.setAttribute("value", aMethod);
-
- let file = $(".requests-menu-file", template);
- file.setAttribute("value", nameWithQuery);
- file.setAttribute("tooltiptext", nameWithQuery);
-
- let domain = $(".requests-menu-domain", template);
- domain.setAttribute("value", hostPort);
- domain.setAttribute("tooltiptext", hostPort);
+ this.updateMenuView(template, 'method', aMethod);
+ this.updateMenuView(template, 'url', aUrl);
let waterfall = $(".requests-menu-waterfall", template);
waterfall.style.backgroundImage = this._cachedWaterfallBackground;
@@ -796,22 +839,48 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
* @param any aValue
* The new value to be shown.
*/
- _updateMenuView: function(aItem, aKey, aValue) {
+ updateMenuView: function(aItem, aKey, aValue) {
+ let target = aItem.target || aItem;
+
switch (aKey) {
+ case "method": {
+ let node = $(".requests-menu-method", target);
+ node.setAttribute("value", aValue);
+ break;
+ }
+ case "url": {
+ let uri;
+ try {
+ uri = nsIURL(aValue);
+ } catch(e) {
+ break; // User input may not make a well-formed url yet.
+ }
+ let nameWithQuery = this._getUriNameWithQuery(uri);
+ let hostPort = this._getUriHostPort(uri);
+
+ let node = $(".requests-menu-file", target);
+ node.setAttribute("value", nameWithQuery);
+ node.setAttribute("tooltiptext", nameWithQuery);
+
+ let domain = $(".requests-menu-domain", target);
+ domain.setAttribute("value", hostPort);
+ domain.setAttribute("tooltiptext", hostPort);
+ break;
+ }
case "status": {
- let node = $(".requests-menu-status", aItem.target);
+ let node = $(".requests-menu-status", target);
node.setAttribute("code", aValue);
break;
}
case "statusText": {
- let node = $(".requests-menu-status-and-method", aItem.target);
+ let node = $(".requests-menu-status-and-method", target);
node.setAttribute("tooltiptext", aValue);
break;
}
case "contentSize": {
let kb = aValue / 1024;
let size = L10N.numberWithDecimals(kb, CONTENT_SIZE_DECIMALS);
- let node = $(".requests-menu-size", aItem.target);
+ let node = $(".requests-menu-size", target);
let text = L10N.getFormatStr("networkMenu.sizeKB", size);
node.setAttribute("value", text);
node.setAttribute("tooltiptext", text);
@@ -819,14 +888,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
case "mimeType": {
let type = this._getAbbreviatedMimeType(aValue);
- let node = $(".requests-menu-type", aItem.target);
+ let node = $(".requests-menu-type", target);
let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
node.setAttribute("value", text);
node.setAttribute("tooltiptext", aValue);
break;
}
case "totalTime": {
- let node = $(".requests-menu-timings-total", aItem.target);
+ let node = $(".requests-menu-timings-total", target);
let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer
node.setAttribute("value", text);
node.setAttribute("tooltiptext", text);
@@ -1089,10 +1158,10 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
*/
_onSelect: function({ detail: item }) {
if (item) {
- NetMonitorView.NetworkDetails.populate(item.attachment);
- NetMonitorView.NetworkDetails.toggle(true);
+ NetMonitorView.Sidebar.populate(item.attachment);
+ NetMonitorView.Sidebar.toggle(true);
} else {
- NetMonitorView.NetworkDetails.toggle(false);
+ NetMonitorView.Sidebar.toggle(false);
}
},
@@ -1104,6 +1173,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
drain("resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
},
+ /**
+ * Handle the context menu opening. Hide items if no request is selected.
+ */
+ _onContextShowing: function() {
+ let element = $("#request-menu-context-resend");
+ element.hidden = !this.selectedItem || this.selectedItem.attachment.isCustom;
+ },
+
/**
* Checks if the specified unix time is the first one to be known of,
* and saves it if so.
@@ -1239,6 +1316,162 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
_resizeTimeout: null
});
+/**
+ * Functions handling the sidebar details view.
+ */
+function SidebarView() {
+ dumpn("SidebarView was instantiated");
+}
+
+SidebarView.prototype = {
+ /**
+ * Sets this view hidden or visible. It's visible by default.
+ *
+ * @param boolean aVisibleFlag
+ * Specifies the intended visibility.
+ */
+ toggle: function(aVisibleFlag) {
+ NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag });
+ NetMonitorView.RequestsMenu._flushWaterfallViews(true);
+ },
+
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object aData
+ * The data source (this should be the attachment of a request item).
+ */
+ populate: function(aData) {
+ if (aData.isCustom) {
+ NetMonitorView.CustomRequest.populate(aData);
+ $("#details-pane").selectedIndex = 0;
+ } else {
+ NetMonitorView.NetworkDetails.populate(aData);
+ $("#details-pane").selectedIndex = 1;
+ }
+ },
+
+ /**
+ * Hides this container.
+ */
+ reset: function() {
+ this.toggle(false);
+ }
+}
+
+/**
+ * Functions handling the custom request view.
+ */
+function CustomRequestView() {
+ dumpn("CustomRequestView was instantiated");
+}
+
+CustomRequestView.prototype = {
+ /**
+ * Populates this view with the specified data.
+ *
+ * @param object aData
+ * The data source (this should be the attachment of a request item).
+ */
+ populate: function(aData) {
+ $("#custom-url-value").value = aData.url;
+ $("#custom-method-value").value = aData.method;
+ $("#custom-headers-value").value =
+ writeHeaderText(aData.requestHeaders.headers);
+
+ if (aData.requestPostData) {
+ let body = aData.requestPostData.postData.text;
+
+ gNetwork.getString(body).then((aString) => {
+ $("#custom-postdata-value").value = aString;
+ });
+ }
+
+ this.updateCustomQuery(aData.url);
+ },
+
+ /**
+ * Handle user input in the custom request form.
+ *
+ * @param object aField
+ * the field that the user updated.
+ */
+ onUpdate: function(aField) {
+ let selectedItem = NetMonitorView.RequestsMenu.selectedItem;
+ let field = aField;
+ let value;
+
+ switch(aField) {
+ case 'method':
+ value = $("#custom-method-value").value.trim();
+ selectedItem.attachment.method = value;
+ break;
+ case 'url':
+ value = $("#custom-url-value").value;
+ this.updateCustomQuery(value);
+ selectedItem.attachment.url = value;
+ break;
+ case 'query':
+ let query = $("#custom-query-value").value;
+ this.updateCustomUrl(query);
+ field = 'url';
+ value = $("#custom-url-value").value
+ selectedItem.attachment.url = value;
+ break;
+ case 'body':
+ value = $("#custom-postdata-value").value;
+ selectedItem.attachment.requestPostData = {
+ postData: {
+ text: value
+ }
+ };
+ break;
+ case 'headers':
+ let headersText = $("#custom-headers-value").value;
+ value = parseHeaderText(headersText);
+ selectedItem.attachment.requestHeaders = {
+ headers: value
+ };
+ break;
+ }
+
+ NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value);
+ },
+
+ /**
+ * Update the query string field based on the url.
+ *
+ * @param object aUrl
+ * url to extract query string from.
+ */
+ updateCustomQuery: function(aUrl) {
+ let paramsArray = parseQueryString(nsIURL(aUrl).query);
+ if (!paramsArray) {
+ $("#custom-query").hidden = true;
+ return;
+ }
+ $("#custom-query").hidden = false;
+ $("#custom-query-value").value = writeQueryText(paramsArray);
+ },
+
+ /**
+ * Update the url based on the query string field.
+ *
+ * @param object aQueryText
+ * contents of the query string field.
+ */
+ updateCustomUrl: function(aQueryText) {
+ let params = parseQueryText(aQueryText);
+ let queryString = writeQueryString(params);
+
+ let url = $("#custom-url-value").value;
+ let oldQuery = nsIURL(url).query;
+ let path = url.replace(oldQuery, queryString);
+
+ $("#custom-url-value").value = path;
+ }
+}
+
/**
* Functions handling the requests details view.
*/
@@ -1253,9 +1486,9 @@ NetworkDetailsView.prototype = {
* Initialization function, called when the network monitor is started.
*/
initialize: function() {
- dumpn("Initializing the RequestsMenuView");
+ dumpn("Initializing the NetworkDetailsView");
- this.widget = $("#details-pane");
+ this.widget = $("#event-details-pane");
this._headers = new VariablesView($("#all-headers"),
Heritage.extend(GENERIC_VARIABLES_VIEW_SETTINGS, {
@@ -1292,25 +1525,13 @@ NetworkDetailsView.prototype = {
* Destruction function, called when the network monitor is closed.
*/
destroy: function() {
- dumpn("Destroying the SourcesView");
+ dumpn("Destroying the NetworkDetailsView");
},
/**
- * Sets this view hidden or visible. It's visible by default.
- *
- * @param boolean aVisibleFlag
- * Specifies the intended visibility.
- */
- toggle: function(aVisibleFlag) {
- NetMonitorView.toggleDetailsPane({ visible: aVisibleFlag });
- NetMonitorView.RequestsMenu._flushWaterfallViews(true);
- },
-
- /**
- * Hides and resets this container (removes all the networking information).
+ * Resets this container (removes all the networking information).
*/
reset: function() {
- this.toggle(false);
this._dataSrc = null;
},
@@ -1581,21 +1802,14 @@ NetworkDetailsView.prototype = {
*
* @param string aName
* The type of params to populate (get or post).
- * @param string aParams
+ * @param string aQueryString
* A query string of params (e.g. "?foo=bar&baz=42").
*/
- _addParams: function(aName, aParams) {
- // Make sure there's at least one param available.
- if (!aParams || !aParams.contains("=")) {
+ _addParams: function(aName, aQueryString) {
+ let paramsArray = parseQueryString(aQueryString);
+ if (!paramsArray) {
return;
}
- // Turn the params string into an array containing { name: value } tuples.
- let paramsArray = aParams.replace(/^[?&]/, "").split("&").map((e) =>
- let (param = e.split("=")) {
- name: NetworkHelper.convertToUnicode(unescape(param[0])),
- value: NetworkHelper.convertToUnicode(unescape(param[1]))
- });
-
let paramsScope = this._params.addScope(aName);
paramsScope.expanded = true;
@@ -1807,6 +2021,110 @@ function nsIURL(aUrl, aStore = nsIURL.store) {
}
nsIURL.store = new Map();
+/**
+ * Parse a url's query string into its components
+ *
+ * @param string aQueryString
+ * The query part of a url
+ * @return array
+ * Array of query params {name, value}
+ */
+function parseQueryString(aQueryString) {
+ // Make sure there's at least one param available.
+ if (!aQueryString || !aQueryString.contains("=")) {
+ return;
+ }
+ // Turn the params string into an array containing { name: value } tuples.
+ let paramsArray = aQueryString.replace(/^[?&]/, "").split("&").map((e) =>
+ let (param = e.split("=")) {
+ name: NetworkHelper.convertToUnicode(unescape(param[0])),
+ value: NetworkHelper.convertToUnicode(unescape(param[1]))
+ });
+ return paramsArray;
+}
+
+/**
+ * Parse text representation of HTTP headers.
+ *
+ * @param string aText
+ * Text of headers
+ * @return array
+ * Array of headers info {name, value}
+ */
+function parseHeaderText(aText) {
+ return parseRequestText(aText, ":");
+}
+
+/**
+ * Parse readable text list of a query string.
+ *
+ * @param string aText
+ * Text of query string represetation
+ * @return array
+ * Array of query params {name, value}
+ */
+function parseQueryText(aText) {
+ return parseRequestText(aText, "=");
+}
+
+/**
+ * Parse a text representation of a name:value list with
+ * the given name:value divider character.
+ *
+ * @param string aText
+ * Text of list
+ * @return array
+ * Array of headers info {name, value}
+ */
+function parseRequestText(aText, aDivider) {
+ let regex = new RegExp("(.+?)\\" + aDivider + "\\s*(.+)");
+ let pairs = [];
+ for (let line of aText.split("\n")) {
+ let matches;
+ if (matches = regex.exec(line)) {
+ let [, name, value] = matches;
+ pairs.push({name: name, value: value});
+ }
+ }
+ return pairs;
+}
+
+/**
+ * Write out a list of headers into a chunk of text
+ *
+ * @param array aHeaders
+ * Array of headers info {name, value}
+ * @return string aText
+ * List of headers in text format
+ */
+function writeHeaderText(aHeaders) {
+ return [(name + ": " + value) for ({name, value} of aHeaders)].join("\n");
+}
+
+/**
+ * Write out a list of query params into a chunk of text
+ *
+ * @param array aParams
+ * Array of query params {name, value}
+ * @return string
+ * List of query params in text format
+ */
+function writeQueryText(aParams) {
+ return [(name + "=" + value) for ({name, value} of aParams)].join("\n");
+}
+
+/**
+ * Write out a list of query params into a query string
+ *
+ * @param array aParams
+ * Array of query params {name, value}
+ * @return string
+ * Query string that can be appended to a url.
+ */
+function writeQueryString(aParams) {
+ return [(name + "=" + value) for ({name, value} of aParams)].join("&");
+}
+
/**
* Helper for draining a rapid succession of events and invoking a callback
* once everything settles down.
@@ -1822,4 +2140,6 @@ drain.store = new Map();
*/
NetMonitorView.Toolbar = new ToolbarView();
NetMonitorView.RequestsMenu = new RequestsMenuView();
+NetMonitorView.Sidebar = new SidebarView();
+NetMonitorView.CustomRequest = new CustomRequestView();
NetMonitorView.NetworkDetails = new NetworkDetailsView();
diff --git a/browser/devtools/netmonitor/netmonitor.css b/browser/devtools/netmonitor/netmonitor.css
index cae41f8aa2e..ce087d7dca1 100644
--- a/browser/devtools/netmonitor/netmonitor.css
+++ b/browser/devtools/netmonitor/netmonitor.css
@@ -15,6 +15,10 @@
overflow: auto;
}
+#custom-pane {
+ overflow: auto;
+}
+
#timings-summary-blocked {
display: none; /* This doesn't work yet. */
}
diff --git a/browser/devtools/netmonitor/netmonitor.xul b/browser/devtools/netmonitor/netmonitor.xul
index 06f4f1e09ec..48165c5376f 100644
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -17,6 +17,16 @@
+